diff --git a/Sources/Storage/StorageBucketApi.swift b/Sources/Storage/StorageBucketApi.swift index 4e8cfba8..003a4382 100644 --- a/Sources/Storage/StorageBucketApi.swift +++ b/Sources/Storage/StorageBucketApi.swift @@ -16,7 +16,7 @@ public class StorageBucketApi: StorageApi { /// Retrieves the details of an existing Storage bucket. /// - Parameters: /// - id: The unique identifier of the bucket you would like to retrieve. - public func getBucket(id: String) async throws -> Bucket { + public func getBucket(_ id: String) async throws -> Bucket { try await execute(Request(path: "/bucket/\(id)", method: .get)) .decoded(decoder: configuration.decoder) } @@ -32,7 +32,7 @@ public class StorageBucketApi: StorageApi { /// Creates a new Storage bucket. /// - Parameters: /// - id: A unique identifier for the bucket you are creating. - public func createBucket(id: String, options: BucketOptions = .init()) async throws { + public func createBucket(_ id: String, options: BucketOptions = .init()) async throws { try await execute( Request( path: "/bucket", @@ -53,7 +53,7 @@ public class StorageBucketApi: StorageApi { /// Updates a Storage bucket /// - Parameters: /// - id: A unique identifier for the bucket you are updating. - public func updateBucket(id: String, options: BucketOptions) async throws { + public func updateBucket(_ id: String, options: BucketOptions) async throws { try await execute( Request( path: "/bucket/\(id)", @@ -74,7 +74,7 @@ public class StorageBucketApi: StorageApi { /// Removes all objects inside a single bucket. /// - Parameters: /// - id: The unique identifier of the bucket you would like to empty. - public func emptyBucket(id: String) async throws { + public func emptyBucket(_ id: String) async throws { try await execute(Request(path: "/bucket/\(id)/empty", method: .post)) } @@ -82,7 +82,7 @@ public class StorageBucketApi: StorageApi { /// You must first `empty()` the bucket. /// - Parameters: /// - id: The unique identifier of the bucket you would like to delete. - public func deleteBucket(id: String) async throws { + public func deleteBucket(_ id: String) async throws { try await execute(Request(path: "/bucket/\(id)", method: .delete)) } } diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index cc125505..d3aba5e6 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -142,6 +142,7 @@ public class StorageFileApi: StorageApi { /// `folder/image.png`. /// - expiresIn: The number of seconds until the signed URL expires. For example, `60` for a URL /// which is valid for one minute. + /// - download: Trigger a download with the specified file name. /// - transform: Transform the asset before serving it to the client. public func createSignedURL( path: String, @@ -195,6 +196,29 @@ public class StorageFileApi: StorageApi { return signedURL } + /// Create signed url to download file without requiring permissions. This URL can be valid for a + /// set number of seconds. + /// - Parameters: + /// - path: The file path to be downloaded, including the current file name. For example + /// `folder/image.png`. + /// - expiresIn: The number of seconds until the signed URL expires. For example, `60` for a URL + /// which is valid for one minute. + /// - download: Trigger a download with the default file name. + /// - transform: Transform the asset before serving it to the client. + public func createSignedURL( + path: String, + expiresIn: Int, + download: Bool, + transform: TransformOptions? = nil + ) async throws -> URL { + try await createSignedURL( + path: path, + expiresIn: expiresIn, + download: download ? "" : nil, + transform: transform + ) + } + /// Deletes files within the same bucket /// - Parameters: /// - paths: An array of files to be deletes, including the path and file name. For example @@ -252,13 +276,11 @@ public class StorageFileApi: StorageApi { /// Returns a public url for an asset. /// - Parameters: /// - path: The file path to the asset. For example `folder/image.png`. - /// - download: Whether the asset should be downloaded. - /// - fileName: If specified, the file name for the asset that is downloaded. + /// - download: Trigger a download with the specified file name. /// - options: Transform the asset before retrieving it on the client. public func getPublicURL( path: String, - download: Bool = false, - fileName: String = "", + download: String? = nil, options: TransformOptions? = nil ) throws -> URL { var queryItems: [URLQueryItem] = [] @@ -268,8 +290,8 @@ public class StorageFileApi: StorageApi { throw URLError(.badURL) } - if download { - queryItems.append(URLQueryItem(name: "download", value: fileName)) + if let download { + queryItems.append(URLQueryItem(name: "download", value: download)) } if let optionsQueryItems = options?.queryItems { @@ -287,6 +309,19 @@ public class StorageFileApi: StorageApi { return generatedUrl } + + /// Returns a public url for an asset. + /// - Parameters: + /// - path: The file path to the asset. For example `folder/image.png`. + /// - download: Trigger a download with the default file name. + /// - options: Transform the asset before retrieving it on the client. + public func getPublicURL( + path: String, + download: Bool, + options: TransformOptions? = nil + ) throws -> URL { + try getPublicURL(path: path, download: download ? "" : nil, options: options) + } } private func fileName(fromPath path: String) -> String { diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index 3d67a419..1b7c283c 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -26,7 +26,7 @@ public class SupabaseStorageClient: StorageBucketApi { /// Perform file operation in a bucket. /// - Parameter id: The bucket id to operate on. /// - Returns: StorageFileApi object - public func from(id: String) -> StorageFileApi { + public func from(_ id: String) -> StorageFileApi { StorageFileApi(bucketId: id, configuration: configuration) } } diff --git a/Tests/StorageTests/StorageClientIntegrationTests.swift b/Tests/StorageTests/StorageClientIntegrationTests.swift new file mode 100644 index 00000000..17ed7e74 --- /dev/null +++ b/Tests/StorageTests/StorageClientIntegrationTests.swift @@ -0,0 +1,159 @@ +// +// StorageClientIntegrationTests.swift +// +// +// Created by Guilherme Souza on 04/11/23. +// + +@testable import Storage +import XCTest + +final class StorageClientIntegrationTests: XCTestCase { + static var apiKey: String { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" + } + + static var supabaseURL: String { + "http://localhost:54321/storage/v1" + } + + let bucketId = "tests" + + let storage = SupabaseStorageClient.test(supabaseURL: supabaseURL, apiKey: apiKey) + + let uploadData = try? Data( + contentsOf: URL( + string: "https://raw.githubusercontent.com/supabase-community/storage-swift/main/README.md" + )! + ) + + override func setUp() async throws { + try await super.setUp() + + try XCTSkipUnless( + ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil, + "INTEGRATION_TESTS not defined." + ) + + try? await storage.emptyBucket(bucketId) + try? await storage.deleteBucket(bucketId) + + try await storage.createBucket(bucketId, options: BucketOptions(public: true)) + } + + func testUpdateBucket() async throws { + var bucket = try await storage.getBucket(bucketId) + XCTAssertTrue(bucket.isPublic) + + try await storage.updateBucket(bucketId, options: BucketOptions(public: false)) + bucket = try await storage.getBucket(bucketId) + XCTAssertFalse(bucket.isPublic) + } + + func testListBuckets() async throws { + let buckets = try await storage.listBuckets() + XCTAssertTrue(buckets.contains { $0.id == bucketId }) + } + + func testFileIntegration() async throws { + var files = try await storage.from(bucketId).list() + XCTAssertTrue(files.isEmpty) + + try await uploadTestData() + + files = try await storage.from(bucketId).list() + XCTAssertEqual(files.map(\.name), ["README.md"]) + + let downloadedData = try await storage.from(bucketId).download(path: "README.md") + XCTAssertEqual(downloadedData, uploadData) + + try await storage.from(bucketId).move(from: "README.md", to: "README_2.md") + + var searchedFiles = try await storage.from(bucketId) + .list(options: .init(search: "README.md")) + XCTAssertTrue(searchedFiles.isEmpty) + + try await storage.from(bucketId).copy(from: "README_2.md", to: "README.md") + searchedFiles = try await storage.from(bucketId).list(options: .init(search: "README.md")) + XCTAssertEqual(searchedFiles.map(\.name), ["README.md"]) + + let removedFiles = try await storage.from(bucketId).remove(paths: ["README_2.md"]) + XCTAssertEqual(removedFiles.map(\.name), ["README_2.md"]) + } + + func testGetPublicURL() async throws { + try await uploadTestData() + + let path = "README.md" + + let baseUrl = try storage.from(bucketId).getPublicURL(path: path) + XCTAssertEqual(baseUrl.absoluteString, "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)") + + let baseUrlWithDownload = try storage.from(bucketId).getPublicURL( + path: path, + download: true + ) + XCTAssertEqual( + baseUrlWithDownload.absoluteString, + "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=" + ) + + let baseUrlWithDownloadAndFileName = try storage.from(bucketId).getPublicURL( + path: path, download: "test" + ) + XCTAssertEqual( + baseUrlWithDownloadAndFileName.absoluteString, + "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=test" + ) + + let baseUrlWithAllOptions = try storage.from(bucketId).getPublicURL( + path: path, download: "test", + options: TransformOptions(width: 300, height: 300) + ) + XCTAssertEqual( + baseUrlWithAllOptions.absoluteString, + "\(Self.supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" + ) + } + + func testCreateSignedURL() async throws { + try await uploadTestData() + + let path = "README.md" + + let url = try await storage.from(bucketId).createSignedURL( + path: path, + expiresIn: 60, + download: "README_local.md" + ) + let components = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true)) + + let downloadQuery = components.queryItems?.first(where: { $0.name == "download" }) + XCTAssertEqual(downloadQuery?.value, "README_local.md") + XCTAssertEqual(components.path, "/storage/v1/object/sign/\(bucketId)/\(path)") + } + + func testUpdate() async throws { + try await uploadTestData() + + let dataToUpdate = try? Data( + contentsOf: URL( + string: "https://raw.githubusercontent.com/supabase-community/supabase-swift/master/README.md" + )! + ) + + try await storage.from(bucketId).update( + path: "README.md", + file: File(name: "README.md", data: dataToUpdate ?? Data(), fileName: nil, contentType: nil) + ) + } + + private func uploadTestData() async throws { + let file = File( + name: "README.md", data: uploadData ?? Data(), fileName: "README.md", contentType: "text/html" + ) + _ = try await storage.from(bucketId).upload( + path: "README.md", file: file, fileOptions: FileOptions(cacheControl: "3600") + ) + } +} diff --git a/Tests/StorageTests/SupabaseStorageClient+Test.swift b/Tests/StorageTests/SupabaseStorageClient+Test.swift new file mode 100644 index 00000000..0beffb77 --- /dev/null +++ b/Tests/StorageTests/SupabaseStorageClient+Test.swift @@ -0,0 +1,23 @@ +// +// SupabaseStorageClient+Test.swift +// +// +// Created by Guilherme Souza on 04/11/23. +// + +import Foundation +import Storage + +extension SupabaseStorageClient { + static func test(supabaseURL: String, apiKey: String) -> SupabaseStorageClient { + SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: supabaseURL)!, + headers: [ + "Authorization": "Bearer \(apiKey)", + "apikey": apiKey, + ] + ) + ) + } +} diff --git a/Tests/StorageTests/SupabaseStorageTests.swift b/Tests/StorageTests/SupabaseStorageTests.swift index ec4e5995..fb7a67a3 100644 --- a/Tests/StorageTests/SupabaseStorageTests.swift +++ b/Tests/StorageTests/SupabaseStorageTests.swift @@ -8,159 +8,44 @@ import XCTest #endif final class SupabaseStorageTests: XCTestCase { - static var apiKey: String { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" - } - - static var supabaseURL: String { - "http://localhost:54321/storage/v1" - } - + let supabaseURL = "http://localhost:54321/storage/v1" let bucketId = "tests" - let storage = SupabaseStorageClient( - configuration: StorageClientConfiguration( - url: URL(string: supabaseURL)!, - headers: [ - "Authorization": "Bearer \(apiKey)", - "apikey": apiKey, - ] - ) - ) - - let uploadData = try? Data( - contentsOf: URL( - string: "https://raw.githubusercontent.com/supabase-community/storage-swift/main/README.md" - )! + lazy var storage = SupabaseStorageClient.test( + supabaseURL: supabaseURL, + apiKey: "test.api.key" ) - override func setUp() async throws { - try await super.setUp() - - try XCTSkipUnless( - ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil, - "INTEGRATION_TESTS not defined." - ) - - try? await storage.emptyBucket(id: bucketId) - try? await storage.deleteBucket(id: bucketId) - - try await storage.createBucket(id: bucketId, options: BucketOptions(public: true)) - } - - func testUpdateBucket() async throws { - var bucket = try await storage.getBucket(id: bucketId) - XCTAssertTrue(bucket.isPublic) - - try await storage.updateBucket(id: bucketId, options: BucketOptions(public: false)) - bucket = try await storage.getBucket(id: bucketId) - XCTAssertFalse(bucket.isPublic) - } - - func testListBuckets() async throws { - let buckets = try await storage.listBuckets() - XCTAssertTrue(buckets.contains { $0.id == bucketId }) - } - - func testFileIntegration() async throws { - var files = try await storage.from(id: bucketId).list() - XCTAssertTrue(files.isEmpty) - - try await uploadTestData() - - files = try await storage.from(id: bucketId).list() - XCTAssertEqual(files.map(\.name), ["README.md"]) - - let downloadedData = try await storage.from(id: bucketId).download(path: "README.md") - XCTAssertEqual(downloadedData, uploadData) - - try await storage.from(id: bucketId).move(from: "README.md", to: "README_2.md") - - var searchedFiles = try await storage.from(id: bucketId) - .list(options: .init(search: "README.md")) - XCTAssertTrue(searchedFiles.isEmpty) - - try await storage.from(id: bucketId).copy(from: "README_2.md", to: "README.md") - searchedFiles = try await storage.from(id: bucketId).list(options: .init(search: "README.md")) - XCTAssertEqual(searchedFiles.map(\.name), ["README.md"]) - - let removedFiles = try await storage.from(id: bucketId).remove(paths: ["README_2.md"]) - XCTAssertEqual(removedFiles.map(\.name), ["README_2.md"]) - } - func testGetPublicURL() async throws { - try await uploadTestData() - let path = "README.md" - let baseUrl = try storage.from(id: bucketId).getPublicURL(path: path) - XCTAssertEqual(baseUrl.absoluteString, "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)") + let baseUrl = try storage.from(bucketId).getPublicURL(path: path) + XCTAssertEqual(baseUrl.absoluteString, "\(supabaseURL)/object/public/\(bucketId)/\(path)") - let baseUrlWithDownload = try storage.from(id: bucketId).getPublicURL( + let baseUrlWithDownload = try storage.from(bucketId).getPublicURL( path: path, download: true ) XCTAssertEqual( baseUrlWithDownload.absoluteString, - "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=" + "\(supabaseURL)/object/public/\(bucketId)/\(path)?download=" ) - let baseUrlWithDownloadAndFileName = try storage.from(id: bucketId).getPublicURL( - path: path, download: true, fileName: "test" + let baseUrlWithDownloadAndFileName = try storage.from(bucketId).getPublicURL( + path: path, download: "test" ) XCTAssertEqual( baseUrlWithDownloadAndFileName.absoluteString, - "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=test" + "\(supabaseURL)/object/public/\(bucketId)/\(path)?download=test" ) - let baseUrlWithAllOptions = try storage.from(id: bucketId).getPublicURL( - path: path, download: true, fileName: "test", + let baseUrlWithAllOptions = try storage.from(bucketId).getPublicURL( + path: path, download: "test", options: TransformOptions(width: 300, height: 300) ) XCTAssertEqual( baseUrlWithAllOptions.absoluteString, - "\(Self.supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" - ) - } - - func testCreateSignedURL() async throws { - try await uploadTestData() - - let path = "README.md" - - let url = try await storage.from(id: bucketId).createSignedURL( - path: path, - expiresIn: 60, - download: "README_local.md" - ) - let components = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true)) - - let downloadQuery = components.queryItems?.first(where: { $0.name == "download" }) - XCTAssertEqual(downloadQuery?.value, "README_local.md") - XCTAssertEqual(components.path, "/storage/v1/object/sign/\(bucketId)/\(path)") - } - - func testUpdate() async throws { - try await uploadTestData() - - let dataToUpdate = try? Data( - contentsOf: URL( - string: "https://raw.githubusercontent.com/supabase-community/supabase-swift/master/README.md" - )! - ) - - try await storage.from(id: bucketId).update( - path: "README.md", - file: File(name: "README.md", data: dataToUpdate ?? Data(), fileName: nil, contentType: nil) - ) - } - - private func uploadTestData() async throws { - let file = File( - name: "README.md", data: uploadData ?? Data(), fileName: "README.md", contentType: "text/html" - ) - _ = try await storage.from(id: bucketId).upload( - path: "README.md", file: file, fileOptions: FileOptions(cacheControl: "3600") + "\(supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" ) } }