From 43dadc0591b1e1304f3b13e506849e7f7b556583 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 21 Aug 2024 15:08:35 -0300 Subject: [PATCH 01/10] feat(storage): add methods for uploading file URL --- Package.swift | 9 +- Sources/Storage/Deprecated.swift | 105 ++++++++++++ Sources/Storage/Helpers.swift | 77 ++++++--- Sources/Storage/MultipartFile.swift | 64 ------- Sources/Storage/StorageApi.swift | 9 +- Sources/Storage/StorageBucketApi.swift | 2 +- Sources/Storage/StorageFileApi.swift | 158 +++++++++++++----- Sources/Storage/SupabaseStorage.swift | 4 +- Sources/Storage/Types.swift | 46 ++++- .../xcshareddata/swiftpm/Package.resolved | 9 + .../StorageFileIntegrationTests.swift | 48 +++--- 11 files changed, 370 insertions(+), 161 deletions(-) delete mode 100644 Sources/Storage/MultipartFile.swift diff --git a/Package.swift b/Package.swift index 95e9b7d63..3e5822151 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let package = Package( .library(name: "Supabase", targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"]), ], dependencies: [ + .package(url: "https://github.com/grdsdev/MultipartFormData", from: "0.1.0"), .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"), .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), @@ -127,7 +128,13 @@ let package = Package( "TestHelpers", ] ), - .target(name: "Storage", dependencies: ["Helpers"]), + .target( + name: "Storage", + dependencies: [ + "MultipartFormData", + "Helpers", + ] + ), .testTarget( name: "StorageTests", dependencies: [ diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift index d5ec8d5d0..865328b94 100644 --- a/Sources/Storage/Deprecated.swift +++ b/Sources/Storage/Deprecated.swift @@ -65,4 +65,109 @@ extension StorageFileApi { ) async throws -> String { try await uploadToSignedURL(path: path, token: token, file: file, options: options).fullPath } + + @available(*, deprecated, renamed: "upload(_:data:options:)") + @discardableResult + public func upload( + path: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> FileUploadResponse { + try await upload(path, data: file, options: options) + } + + @available(*, deprecated, renamed: "update(_:data:options:)") + @discardableResult + public func update( + path: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> FileUploadResponse { + try await update(path, data: file, options: options) + } + + @available(*, deprecated, renamed: "updateToSignedURL(_:token:data:options:)") + @discardableResult + public func uploadToSignedURL( + path: String, + token: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> SignedURLUploadResponse { + try await uploadToSignedURL(path, token: token, data: file, options: options) + } +} + +@available( + *, + deprecated, + message: "File was deprecated and it isn't used in the package anymore, if you're using it on your application, consider replacing it as it will be removed on the next major release." +) +public struct File: Hashable, Equatable { + public var name: String + public var data: Data + public var fileName: String? + public var contentType: String? + + public init(name: String, data: Data, fileName: String?, contentType: String?) { + self.name = name + self.data = data + self.fileName = fileName + self.contentType = contentType + } +} + +@available( + *, + deprecated, + renamed: "MultipartFormData", + message: "FormData was deprecated in favor of MultipartFormData, and it isn't used in the package anymore, if you're using it on your application, consider replacing it as it will be removed on the next major release." +) +public class FormData { + var files: [File] = [] + var boundary: String + + public init(boundary: String = UUID().uuidString) { + self.boundary = boundary + } + + public func append(file: File) { + files.append(file) + } + + public var contentType: String { + "multipart/form-data; boundary=\(boundary)" + } + + public var data: Data { + var data = Data() + + for file in files { + data.append("--\(boundary)\r\n") + data.append("Content-Disposition: form-data; name=\"\(file.name)\"") + if let filename = file.fileName?.replacingOccurrences(of: "\"", with: "_") { + data.append("; filename=\"\(filename)\"") + } + data.append("\r\n") + if let contentType = file.contentType { + data.append("Content-Type: \(contentType)\r\n") + } + data.append("\r\n") + data.append(file.data) + data.append("\r\n") + } + + data.append("--\(boundary)--\r\n") + return data + } +} + +extension Data { + mutating func append(_ string: String) { + let data = string.data( + using: String.Encoding.utf8, + allowLossyConversion: true + ) + append(data!) + } } diff --git a/Sources/Storage/Helpers.swift b/Sources/Storage/Helpers.swift index 3bbdf0d42..382d7250b 100644 --- a/Sources/Storage/Helpers.swift +++ b/Sources/Storage/Helpers.swift @@ -7,38 +7,67 @@ import Foundation -#if canImport(CoreServices) +#if canImport(MobileCoreServices) + import MobileCoreServices +#elseif canImport(CoreServices) import CoreServices #endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers -#endif -#if os(Linux) || os(Windows) - /// On Linux or Windows this method always returns `application/octet-stream`. - func mimeTypeForExtension(_: String) -> String { - "application/octet-stream" + func mimeType(forPathExtension pathExtension: String) -> String { + #if swift(>=5.9) + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) { + return UTType(filenameExtension: pathExtension)?.preferredMIMEType + ?? "application/octet-stream" + } else { + if let id = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, pathExtension as CFString, nil + )?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? + .takeRetainedValue() + { + return contentType as String + } + + return "application/octet-stream" + } + #else + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { + return UTType(filenameExtension: pathExtension)?.preferredMIMEType + ?? "application/octet-stream" + } else { + if let id = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, pathExtension as CFString, nil + )?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? + .takeRetainedValue() + { + return contentType as String + } + + return "application/octet-stream" + } + #endif } #else - func mimeTypeForExtension(_ fileExtension: String) -> String { - if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, visionOS 1.0, *) { - return UTType(filenameExtension: fileExtension)?.preferredMIMEType ?? "application/octet-stream" - } else { - guard - let type = UTTypeCreatePreferredIdentifierForTag( - kUTTagClassFilenameExtension, - fileExtension as NSString, - nil - )?.takeUnretainedValue(), - let mimeType = UTTypeCopyPreferredTagWithClass( - type, - kUTTagClassMIMEType - )?.takeUnretainedValue() - else { return "application/octet-stream" } - - return mimeType as String - } + + // MARK: - Private - Mime Type + + func mimeType(forPathExtension pathExtension: String) -> String { + #if canImport(CoreServices) || canImport(MobileCoreServices) + if let id = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, pathExtension as CFString, nil + )?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)? + .takeRetainedValue() + { + return contentType as String + } + #endif + + return "application/octet-stream" } #endif diff --git a/Sources/Storage/MultipartFile.swift b/Sources/Storage/MultipartFile.swift deleted file mode 100644 index ad98d41f1..000000000 --- a/Sources/Storage/MultipartFile.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation - -public struct File: Hashable, Equatable { - public var name: String - public var data: Data - public var fileName: String? - public var contentType: String? - - public init(name: String, data: Data, fileName: String?, contentType: String?) { - self.name = name - self.data = data - self.fileName = fileName - self.contentType = contentType - } -} - -public class FormData { - var files: [File] = [] - var boundary: String - - public init(boundary: String = UUID().uuidString) { - self.boundary = boundary - } - - public func append(file: File) { - files.append(file) - } - - public var contentType: String { - "multipart/form-data; boundary=\(boundary)" - } - - public var data: Data { - var data = Data() - - for file in files { - data.append("--\(boundary)\r\n") - data.append("Content-Disposition: form-data; name=\"\(file.name)\"") - if let filename = file.fileName?.replacingOccurrences(of: "\"", with: "_") { - data.append("; filename=\"\(filename)\"") - } - data.append("\r\n") - if let contentType = file.contentType { - data.append("Content-Type: \(contentType)\r\n") - } - data.append("\r\n") - data.append(file.data) - data.append("\r\n") - } - - data.append("--\(boundary)--\r\n") - return data - } -} - -extension Data { - mutating func append(_ string: String) { - let data = string.data( - using: String.Encoding.utf8, - allowLossyConversion: true - ) - append(data!) - } -} diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index 73a98880f..74bf70211 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -1,5 +1,6 @@ import Foundation import Helpers +import class MultipartFormData.MultipartFormData #if canImport(FoundationNetworking) import FoundationNetworking @@ -52,10 +53,10 @@ extension HTTPRequest { url: URL, method: HTTPMethod, query: [URLQueryItem], - formData: FormData, + formData: MultipartFormData, options: FileOptions, headers: HTTPHeaders = [:] - ) { + ) throws { var headers = headers if headers["Content-Type"] == nil { headers["Content-Type"] = formData.contentType @@ -63,12 +64,12 @@ extension HTTPRequest { if headers["Cache-Control"] == nil { headers["Cache-Control"] = "max-age=\(options.cacheControl)" } - self.init( + try self.init( url: url, method: method, query: query, headers: headers, - body: formData.data + body: formData.encode() ) } } diff --git a/Sources/Storage/StorageBucketApi.swift b/Sources/Storage/StorageBucketApi.swift index 3351be887..61473a1af 100644 --- a/Sources/Storage/StorageBucketApi.swift +++ b/Sources/Storage/StorageBucketApi.swift @@ -6,7 +6,7 @@ import Helpers #endif /// Storage Bucket API -public class StorageBucketApi: StorageApi { +public class StorageBucketApi: StorageApi, @unchecked Sendable { /// Retrieves the details of all Storage buckets within an existing product. public func listBuckets() async throws -> [Bucket] { try await execute( diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 8093464c6..755c2bf15 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -1,5 +1,6 @@ import Foundation import Helpers +import class MultipartFormData.MultipartFormData #if canImport(FoundationNetworking) import FoundationNetworking @@ -15,9 +16,9 @@ let DEFAULT_SEARCH_OPTIONS = SearchOptions( ) /// Supabase Storage File API -public class StorageFileApi: StorageApi { +public class StorageFileApi: StorageApi, @unchecked Sendable { /// The bucket id to operate on. - var bucketId: String + let bucketId: String init(bucketId: String, configuration: StorageClientConfiguration) { self.bucketId = bucketId @@ -35,22 +36,16 @@ public class StorageFileApi: StorageApi { func uploadOrUpdate( method: HTTPMethod, path: String, - file: Data, + formData: MultipartFormData, options: FileOptions ) async throws -> FileUploadResponse { - let contentType = options.contentType ?? mimeTypeForExtension(path.pathExtension) - var headers = HTTPHeaders([ - "x-upsert": "\(options.upsert)", - ]) - - headers["duplex"] = options.duplex + var headers = HTTPHeaders() - let fileName = path.fileName + if method == .post { + headers.update(name: "x-upsert", value: "\(options.upsert)") + } - let form = FormData() - form.append( - file: File(name: fileName, data: file, fileName: fileName, contentType: contentType) - ) + headers["duplex"] = options.duplex struct UploadResponse: Decodable { let Key: String @@ -62,7 +57,7 @@ public class StorageFileApi: StorageApi { url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), method: method, query: [], - formData: form, + formData: formData, options: options, headers: headers ) @@ -80,30 +75,70 @@ public class StorageFileApi: StorageApi { /// - Parameters: /// - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The /// bucket must already exist before attempting to upload. - /// - file: The Data to be stored in the bucket. + /// - data: The Data to be stored in the bucket. /// - options: HTTP headers. For example `cacheControl` @discardableResult public func upload( - path: String, - file: Data, + _ path: String, + data: Data, + options: FileOptions = FileOptions() + ) async throws -> FileUploadResponse { + let formData = MultipartFormData() + formData.append( + data, + withName: path.fileName, + mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) + ) + return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) + } + + @discardableResult + public func upload( + _ path: String, + fileURL: Data, options: FileOptions = FileOptions() ) async throws -> FileUploadResponse { - try await uploadOrUpdate(method: .post, path: path, file: file, options: options) + let formData = MultipartFormData() + formData.append(fileURL, withName: path.fileName) + return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) } /// Replaces an existing file at the specified path with a new one. /// - Parameters: /// - path: The relative file path. Should be of the format `folder/subfolder`. The bucket /// already exist before attempting to upload. - /// - file: The Data to be stored in the bucket. + /// - data: The Data to be stored in the bucket. /// - options: HTTP headers. For example `cacheControl` @discardableResult public func update( - path: String, - file: Data, + _ path: String, + data: Data, + options: FileOptions = FileOptions() + ) async throws -> FileUploadResponse { + let formData = MultipartFormData() + formData.append( + data, + withName: path.fileName, + mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) + ) + return try await uploadOrUpdate(method: .put, path: path, formData: formData, options: options) + } + + /// Replaces an existing file at the specified path with a new one. + /// - Parameters: + /// - path: The relative file path. Should be of the format `folder/subfolder`. The bucket + /// already exist before attempting to upload. + /// - fileURL: The file URL to be stored in the bucket. + /// - options: HTTP headers. For example `cacheControl` + @discardableResult + public func update( + _ path: String, + fileURL: URL, options: FileOptions = FileOptions() ) async throws -> FileUploadResponse { - try await uploadOrUpdate(method: .put, path: path, file: file, options: options) + let formData = MultipartFormData() + formData.append(fileURL, withName: path.fileName) + return try await uploadOrUpdate(method: .put, path: path, formData: formData, options: options) } /// Moves an existing file, optionally renaming it at the same time. @@ -358,6 +393,17 @@ public class StorageFileApi: StorageApi { .data } + /// Retrieves the details of an existing file. + public func info(path: String) async throws -> FileObjectV2 { + try await execute( + HTTPRequest( + url: configuration.url.appendingPathComponent("object/info/\(bucketId)/\(path)"), + method: .get + ) + ) + .decoded(decoder: configuration.decoder) + } + /// Returns a public url for an asset. /// - Parameters: /// - path: The file path to the asset. For example `folder/image.png`. @@ -461,35 +507,67 @@ public class StorageFileApi: StorageApi { /// Upload a file with a token generated from ``StorageFileApi/createSignedUploadURL(path:)``. /// - Parameters: - /// - path: The file path, including the file name. Should be of the format - /// `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. + /// - path: The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. /// - token: The token generated from ``StorageFileApi/createSignedUploadURL(path:)``. - /// - file: The Data to be stored in the bucket. + /// - data: The Data to be stored in the bucket. /// - options: HTTP headers, for example `cacheControl`. /// - Returns: A key pointing to stored location. @discardableResult public func uploadToSignedURL( - path: String, + _ path: String, token: String, - file: Data, + data: Data, options: FileOptions = FileOptions() ) async throws -> SignedURLUploadResponse { - let contentType = options.contentType ?? mimeTypeForExtension(path.pathExtension) + let formData = MultipartFormData() + formData.append( + data, + withName: path.fileName, + mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) + ) + return try await _uploadToSignedURL( + path: path, + token: token, + formData: formData, + options: options + ) + } + + /// Upload a file with a token generated from ``StorageFileApi/createSignedUploadURL(path:)``. + /// - Parameters: + /// - path: The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. + /// - token: The token generated from ``StorageFileApi/createSignedUploadURL(path:)``. + /// - fileURL: The file URL to be stored in the bucket. + /// - options: HTTP headers, for example `cacheControl`. + /// - Returns: A key pointing to stored location. + @discardableResult + public func uploadToSignedURL( + _ path: String, + token: String, + fileURL: Data, + options: FileOptions = FileOptions() + ) async throws -> SignedURLUploadResponse { + let formData = MultipartFormData() + formData.append(fileURL, withName: path.fileName) + return try await _uploadToSignedURL( + path: path, + token: token, + formData: formData, + options: options + ) + } + + private func _uploadToSignedURL( + path: String, + token: String, + formData: MultipartFormData, + options: FileOptions + ) async throws -> SignedURLUploadResponse { var headers = HTTPHeaders([ "x-upsert": "\(options.upsert)", ]) headers["duplex"] = options.duplex - let fileName = path.fileName - - let form = FormData() - form.append(file: File( - name: fileName, - data: file, - fileName: fileName, - contentType: contentType - )) - struct UploadResponse: Decodable { let Key: String } @@ -500,7 +578,7 @@ public class StorageFileApi: StorageApi { .appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), method: .put, query: [URLQueryItem(name: "token", value: token)], - formData: form, + formData: formData, options: options, headers: headers ) diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index b2cad2396..ed57a66e3 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -4,7 +4,7 @@ import Helpers public typealias SupabaseLogger = Helpers.SupabaseLogger public typealias SupabaseLogMessage = Helpers.SupabaseLogMessage -public struct StorageClientConfiguration { +public struct StorageClientConfiguration: Sendable { public let url: URL public var headers: [String: String] public let encoder: JSONEncoder @@ -29,7 +29,7 @@ public struct StorageClientConfiguration { } } -public class SupabaseStorageClient: StorageBucketApi { +public class SupabaseStorageClient: StorageBucketApi, @unchecked Sendable { /// Perform file operation in a bucket. /// - Parameter id: The bucket id to operate on. /// - Returns: StorageFileApi object diff --git a/Sources/Storage/Types.swift b/Sources/Storage/Types.swift index 14e36edfd..aff88a6b3 100644 --- a/Sources/Storage/Types.swift +++ b/Sources/Storage/Types.swift @@ -57,16 +57,28 @@ public struct FileOptions: Sendable { /// fetch() method. public var duplex: String? + /// The metadata option is an object that allows you to store additional information about the file. + /// This information can be used to filter and search for files. + /// The metadata object can contain any key-value pairs you want to store. + public var metadata: [String: AnyJSON]? + + /// Optionally add extra headers. + public var headers: [String: String]? + public init( cacheControl: String = "3600", contentType: String? = nil, upsert: Bool = false, - duplex: String? = nil + duplex: String? = nil, + metadata: [String: AnyJSON]? = nil, + headers: [String: String]? = nil ) { self.cacheControl = cacheControl self.contentType = contentType self.upsert = upsert self.duplex = duplex + self.metadata = metadata + self.headers = headers } } @@ -166,6 +178,38 @@ public struct FileObject: Identifiable, Hashable, Codable, Sendable { } } +public struct FileObjectV2: Identifiable, Hashable, Decodable, Sendable { + public let id: String + public let version: String + public let name: String + public let bucketId: String + public let updatedAt: Date + public let createdAt: Date + public let lastAccessedAt: Date + public let size: Int? + public let cacheControl: String? + public let contentType: String? + public let etag: String? + public let lastModified: Date? + public let metadata: [String: AnyJSON]? + + enum CodingKeys: String, CodingKey { + case id + case version + case name + case bucketId = "bucket_id" + case updatedAt = "updated_at" + case createdAt = "created_at" + case lastAccessedAt = "last_accessed_at" + case size + case cacheControl = "cache_control" + case contentType = "content_type" + case etag + case lastModified = "last_modified" + case metadata + } +} + public struct Bucket: Identifiable, Hashable, Codable, Sendable { public var id: String public var name: String diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved index 52f217aa0..a92350636 100644 --- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -36,6 +36,15 @@ "version" : "4.1.1" } }, + { + "identity" : "multipartformdata", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grdsdev/MultipartFormData", + "state" : { + "revision" : "ed7abea9cfc6c3b5e77a73fe6842c57a372d2017", + "version" : "0.1.0" + } + }, { "identity" : "svgview", "kind" : "remoteSourceControl", diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index 21e6206e5..8f1a351cc 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -64,7 +64,7 @@ final class StorageFileIntegrationTests: XCTestCase { } func testSignURL() async throws { - _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + _ = try await storage.from(bucketName).upload(uploadPath, data: file) let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000) XCTAssertTrue( @@ -73,7 +73,7 @@ final class StorageFileIntegrationTests: XCTestCase { } func testSignURL_withDownloadQueryString() async throws { - _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + _ = try await storage.from(bucketName).upload(uploadPath, data: file) let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000, download: true) XCTAssertTrue( @@ -83,7 +83,7 @@ final class StorageFileIntegrationTests: XCTestCase { } func testSignURL_withCustomFilenameForDownload() async throws { - _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + _ = try await storage.from(bucketName).upload(uploadPath, data: file) let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000, download: "test.jpg") XCTAssertTrue( @@ -95,9 +95,9 @@ final class StorageFileIntegrationTests: XCTestCase { func testUploadAndUpdateFile() async throws { let file2 = try Data(contentsOf: uploadFileURL("file-2.txt")) - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) - let res = try await storage.from(bucketName).update(path: uploadPath, file: file2) + let res = try await storage.from(bucketName).update(uploadPath, data: file2) XCTAssertEqual(res.path, uploadPath) } @@ -107,7 +107,7 @@ final class StorageFileIntegrationTests: XCTestCase { options: BucketOptions(public: true, fileSizeLimit: "1mb") ) - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) } func testUploadFileThatExceedFileSizeLimit() async throws { @@ -117,7 +117,7 @@ final class StorageFileIntegrationTests: XCTestCase { ) do { - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) XCTFail("Unexpected success") } catch { assertInlineSnapshot(of: error, as: .dump) { @@ -141,8 +141,8 @@ final class StorageFileIntegrationTests: XCTestCase { ) try await storage.from(bucketName).upload( - path: uploadPath, - file: file, + uploadPath, + data: file, options: FileOptions( contentType: "image/jpeg" ) @@ -157,8 +157,8 @@ final class StorageFileIntegrationTests: XCTestCase { do { try await storage.from(bucketName).upload( - path: uploadPath, - file: file, + uploadPath, + data: file, options: FileOptions( contentType: "image/jpeg" ) @@ -192,25 +192,25 @@ final class StorageFileIntegrationTests: XCTestCase { func testCanUploadWithSignedURLForUpload() async throws { let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath) - let uploadRes = try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + let uploadRes = try await storage.from(bucketName).uploadToSignedURL(res.path, token: res.token, data: file) XCTAssertEqual(uploadRes.path, uploadPath) } func testCanUploadOverwritingFilesWithSignedURL() async throws { - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath, options: CreateSignedUploadURLOptions(upsert: true)) - let uploadRes = try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + let uploadRes = try await storage.from(bucketName).uploadToSignedURL(res.path, token: res.token, data: file) XCTAssertEqual(uploadRes.path, uploadPath) } func testCannotUploadToSignedURLTwice() async throws { let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath) - try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + try await storage.from(bucketName).uploadToSignedURL(res.path, token: res.token, data: file) do { - try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + try await storage.from(bucketName).uploadToSignedURL(res.path, token: res.token, data: file) XCTFail("Unexpected success") } catch { assertInlineSnapshot(of: error, as: .dump) { @@ -228,7 +228,7 @@ final class StorageFileIntegrationTests: XCTestCase { } func testListObjects() async throws { - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) let res = try await storage.from(bucketName).list(path: "testpath") XCTAssertEqual(res.count, 1) @@ -237,7 +237,7 @@ final class StorageFileIntegrationTests: XCTestCase { func testMoveObjectToDifferentPath() async throws { let newPath = "testpath/file-moved-\(UUID().uuidString).txt" - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) try await storage.from(bucketName).move(from: uploadPath, to: newPath) } @@ -247,7 +247,7 @@ final class StorageFileIntegrationTests: XCTestCase { try await findOrCreateBucket(name: newBucketName) let newPath = "testpath/file-to-move-\(UUID().uuidString).txt" - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) try await storage.from(bucketName).move( from: uploadPath, @@ -260,7 +260,7 @@ final class StorageFileIntegrationTests: XCTestCase { func testCopyObjectToDifferentPath() async throws { let newPath = "testpath/file-moved-\(UUID().uuidString).txt" - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) try await storage.from(bucketName).copy(from: uploadPath, to: newPath) } @@ -270,7 +270,7 @@ final class StorageFileIntegrationTests: XCTestCase { try await findOrCreateBucket(name: newBucketName) let newPath = "testpath/file-to-copy-\(UUID().uuidString).txt" - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) try await storage.from(bucketName).copy( from: uploadPath, @@ -282,14 +282,14 @@ final class StorageFileIntegrationTests: XCTestCase { } func testDownloadsAnObject() async throws { - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) let res = try await storage.from(bucketName).download(path: uploadPath) XCTAssertGreaterThan(res.count, 0) } func testRemovesAnObject() async throws { - try await storage.from(bucketName).upload(path: uploadPath, file: file) + try await storage.from(bucketName).upload(uploadPath, data: file) let res = try await storage.from(bucketName).remove(paths: [uploadPath]) XCTAssertEqual(res.count, 1) @@ -315,7 +315,7 @@ final class StorageFileIntegrationTests: XCTestCase { func testCreateAndLoadEmptyFolder() async throws { let path = "empty-folder/.placeholder" - try await storage.from(bucketName).upload(path: path, file: Data()) + try await storage.from(bucketName).upload(path, data: Data()) let files = try await storage.from(bucketName).list() assertInlineSnapshot(of: files, as: .json) { From ff44a5803263f627eec5e996ce4da4afd0dc65df Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 22 Aug 2024 05:39:40 -0300 Subject: [PATCH 02/10] feat(storage): add metadata, info and exists method --- Sources/Storage/StorageApi.swift | 5 ++- Sources/Storage/StorageFileApi.swift | 61 +++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index 74bf70211..e2ad1fb5f 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -37,7 +37,10 @@ public class StorageApi: @unchecked Sendable { let response = try await http.send(request) guard (200 ..< 300).contains(response.statusCode) else { - if let error = try? configuration.decoder.decode(StorageError.self, from: response.data) { + if let error = try? configuration.decoder.decode( + StorageError.self, + from: response.data + ) { throw error } diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 755c2bf15..02d2b3e75 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -15,6 +15,12 @@ let DEFAULT_SEARCH_OPTIONS = SearchOptions( ) ) +private let defaultFileOptions = FileOptions( + cacheControl: "3600", + contentType: "text/plain;charset=UTF-8", + upsert: false +) + /// Supabase Storage File API public class StorageFileApi: StorageApi, @unchecked Sendable { /// The bucket id to operate on. @@ -33,20 +39,37 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let signedURL: URL } + private func encodeMetadata(_ metadata: JSONObject) -> Data { + let encoder = AnyJSON.encoder + return (try? encoder.encode(metadata)) ?? "{}".data(using: .utf8)! + } + func uploadOrUpdate( method: HTTPMethod, path: String, formData: MultipartFormData, - options: FileOptions + options: FileOptions? ) async throws -> FileUploadResponse { var headers = HTTPHeaders() + let options = options ?? defaultFileOptions + let metadata = options.metadata + if method == .post { headers.update(name: "x-upsert", value: "\(options.upsert)") } headers["duplex"] = options.duplex + if let metadata { + formData.append(encodeMetadata(metadata), withName: "metadata") + } + + formData.append( + options.cacheControl.data(using: .utf8)!, + withName: "cacheControl" + ) + struct UploadResponse: Decodable { let Key: String let Id: String @@ -404,6 +427,27 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { .decoded(decoder: configuration.decoder) } + /// Checks the existence of file. + public func exists(path: String) async throws -> Bool { + do { + let response = try await execute( + HTTPRequest( + url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), + method: .head + ) + ) + return true + } catch { + if let error = error as? StorageError, let statusCode = error.statusCode, + ["400", "404"].contains(statusCode) + { + return false + } + + throw error + } + } + /// Returns a public url for an asset. /// - Parameters: /// - path: The file path to the asset. For example `folder/image.png`. @@ -517,13 +561,13 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { _ path: String, token: String, data: Data, - options: FileOptions = FileOptions() + options: FileOptions? = nil ) async throws -> SignedURLUploadResponse { let formData = MultipartFormData() formData.append( data, withName: path.fileName, - mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) + mimeType: options?.contentType ?? mimeType(forPathExtension: path.pathExtension) ) return try await _uploadToSignedURL( path: path, @@ -545,7 +589,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { _ path: String, token: String, fileURL: Data, - options: FileOptions = FileOptions() + options: FileOptions? = nil ) async throws -> SignedURLUploadResponse { let formData = MultipartFormData() formData.append(fileURL, withName: path.fileName) @@ -561,13 +605,20 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { path: String, token: String, formData: MultipartFormData, - options: FileOptions + options: FileOptions? ) async throws -> SignedURLUploadResponse { + let options = options ?? defaultFileOptions var headers = HTTPHeaders([ "x-upsert": "\(options.upsert)", ]) headers["duplex"] = options.duplex + if let metadata = options.metadata { + formData.append(encodeMetadata(metadata), withName: "metadata") + } + + formData.append(options.cacheControl.data(using: .utf8)!, withName: "cacheControl") + struct UploadResponse: Decodable { let Key: String } From 6ab2ce5378300f98487ecfe59359bb029ff91af5 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Sat, 24 Aug 2024 07:50:01 -0300 Subject: [PATCH 03/10] add integration tests --- Sources/Storage/StorageFileApi.swift | 29 ++++++++++++++----- .../StorageFileIntegrationTests.swift | 17 +++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 02d2b3e75..d81f4db6d 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -106,10 +106,12 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { data: Data, options: FileOptions = FileOptions() ) async throws -> FileUploadResponse { + let fileName = path.fileName let formData = MultipartFormData() formData.append( data, - withName: path.fileName, + withName: fileName, + fileName: fileName, mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) ) return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) @@ -121,8 +123,9 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { fileURL: Data, options: FileOptions = FileOptions() ) async throws -> FileUploadResponse { + let fileName = path.fileName let formData = MultipartFormData() - formData.append(fileURL, withName: path.fileName) + formData.append(fileURL, withName: fileName, fileName: fileName) return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) } @@ -138,10 +141,12 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { data: Data, options: FileOptions = FileOptions() ) async throws -> FileUploadResponse { + let fileName = path.fileName let formData = MultipartFormData() formData.append( data, - withName: path.fileName, + withName: fileName, + fileName: fileName, mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) ) return try await uploadOrUpdate(method: .put, path: path, formData: formData, options: options) @@ -430,7 +435,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { /// Checks the existence of file. public func exists(path: String) async throws -> Bool { do { - let response = try await execute( + try await execute( HTTPRequest( url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), method: .head @@ -438,9 +443,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { ) return true } catch { - if let error = error as? StorageError, let statusCode = error.statusCode, - ["400", "404"].contains(statusCode) - { + var statusCode: Int? + + if let error = error as? StorageError { + statusCode = error.statusCode.flatMap(Int.init) + } else if let error = error as? HTTPError { + statusCode = error.response.statusCode + } + + if let statusCode, [400, 404].contains(statusCode) { return false } @@ -563,10 +574,12 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { data: Data, options: FileOptions? = nil ) async throws -> SignedURLUploadResponse { + let fileName = path.fileName let formData = MultipartFormData() formData.append( data, - withName: path.fileName, + withName: fileName, + fileName: fileName, mimeType: options?.contentType ?? mimeType(forPathExtension: path.pathExtension) ) return try await _uploadToSignedURL( diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index 8f1a351cc..aa5a903fe 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -329,6 +329,23 @@ final class StorageFileIntegrationTests: XCTestCase { } } + func testInfo() async throws { + try await storage.from(bucketName).upload(uploadPath, data: file) + + let info = try await storage.from(bucketName).info(path: uploadPath) + assertInlineSnapshot(of: info, as: .json) + } + + func testExists() async throws { + try await storage.from(bucketName).upload(uploadPath, data: file) + + var exists = try await storage.from(bucketName).exists(path: uploadPath) + XCTAssertTrue(exists) + + exists = try await storage.from(bucketName).exists(path: "invalid.jpg") + XCTAssertFalse(exists) + } + private func newBucket( prefix: String = "", options: BucketOptions = BucketOptions(public: true) From 4a9e884653af47bff8792627fe547bd59119fd02 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 26 Aug 2024 13:22:58 -0300 Subject: [PATCH 04/10] fix info method --- Sources/Storage/StorageFileApi.swift | 2 +- Sources/Storage/Types.swift | 8 ++++---- Tests/IntegrationTests/StorageFileIntegrationTests.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index d81f4db6d..c704b896e 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -425,7 +425,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { public func info(path: String) async throws -> FileObjectV2 { try await execute( HTTPRequest( - url: configuration.url.appendingPathComponent("object/info/\(bucketId)/\(path)"), + url: configuration.url.appendingPathComponent("object/info/public/\(bucketId)/\(path)"), method: .get ) ) diff --git a/Sources/Storage/Types.swift b/Sources/Storage/Types.swift index aff88a6b3..82874c2fe 100644 --- a/Sources/Storage/Types.swift +++ b/Sources/Storage/Types.swift @@ -182,10 +182,10 @@ public struct FileObjectV2: Identifiable, Hashable, Decodable, Sendable { public let id: String public let version: String public let name: String - public let bucketId: String - public let updatedAt: Date - public let createdAt: Date - public let lastAccessedAt: Date + public let bucketId: String? + public let updatedAt: Date? + public let createdAt: Date? + public let lastAccessedAt: Date? public let size: Int? public let cacheControl: String? public let contentType: String? diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index aa5a903fe..fe84bdf37 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -333,7 +333,7 @@ final class StorageFileIntegrationTests: XCTestCase { try await storage.from(bucketName).upload(uploadPath, data: file) let info = try await storage.from(bucketName).info(path: uploadPath) - assertInlineSnapshot(of: info, as: .json) + XCTAssertEqual(info.name, uploadPath) } func testExists() async throws { From c7f31aa000b89a4258d9b7d21d0ea68c1c364e3c Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 26 Aug 2024 17:04:09 -0300 Subject: [PATCH 05/10] test upload with metadata --- Tests/IntegrationTests/StorageFileIntegrationTests.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index fe84bdf37..f2ae953ec 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -330,10 +330,17 @@ final class StorageFileIntegrationTests: XCTestCase { } func testInfo() async throws { - try await storage.from(bucketName).upload(uploadPath, data: file) + try await storage.from(bucketName).upload( + uploadPath, + data: file, + options: FileOptions( + metadata: ["value": 42] + ) + ) let info = try await storage.from(bucketName).info(path: uploadPath) XCTAssertEqual(info.name, uploadPath) + XCTAssertEqual(info.metadata, ["value": 42]) } func testExists() async throws { From 1865447814827d978463876582292ba7c07e4551 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 26 Aug 2024 17:55:12 -0300 Subject: [PATCH 06/10] support additional headers --- .swiftpm/configuration/Package.resolved | 9 +++++++++ Sources/Storage/StorageFileApi.swift | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.swiftpm/configuration/Package.resolved b/.swiftpm/configuration/Package.resolved index dee6efd6a..5fbb03482 100644 --- a/.swiftpm/configuration/Package.resolved +++ b/.swiftpm/configuration/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "multipartformdata", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grdsdev/MultipartFormData", + "state" : { + "revision" : "ed7abea9cfc6c3b5e77a73fe6842c57a372d2017", + "version" : "0.1.0" + } + }, { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index c704b896e..86f86e8db 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -44,15 +44,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { return (try? encoder.encode(metadata)) ?? "{}".data(using: .utf8)! } - func uploadOrUpdate( + private func _uploadOrUpdate( method: HTTPMethod, path: String, formData: MultipartFormData, options: FileOptions? ) async throws -> FileUploadResponse { - var headers = HTTPHeaders() - let options = options ?? defaultFileOptions + var headers = options.headers.map { HTTPHeaders($0) } ?? HTTPHeaders() + let metadata = options.metadata if method == .post { @@ -114,7 +114,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { fileName: fileName, mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) ) - return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) + return try await _uploadOrUpdate(method: .post, path: path, formData: formData, options: options) } @discardableResult @@ -126,7 +126,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let fileName = path.fileName let formData = MultipartFormData() formData.append(fileURL, withName: fileName, fileName: fileName) - return try await uploadOrUpdate(method: .post, path: path, formData: formData, options: options) + return try await _uploadOrUpdate(method: .post, path: path, formData: formData, options: options) } /// Replaces an existing file at the specified path with a new one. @@ -149,7 +149,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { fileName: fileName, mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension) ) - return try await uploadOrUpdate(method: .put, path: path, formData: formData, options: options) + return try await _uploadOrUpdate(method: .put, path: path, formData: formData, options: options) } /// Replaces an existing file at the specified path with a new one. @@ -166,7 +166,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { ) async throws -> FileUploadResponse { let formData = MultipartFormData() formData.append(fileURL, withName: path.fileName) - return try await uploadOrUpdate(method: .put, path: path, formData: formData, options: options) + return try await _uploadOrUpdate(method: .put, path: path, formData: formData, options: options) } /// Moves an existing file, optionally renaming it at the same time. @@ -621,9 +621,9 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { options: FileOptions? ) async throws -> SignedURLUploadResponse { let options = options ?? defaultFileOptions - var headers = HTTPHeaders([ - "x-upsert": "\(options.upsert)", - ]) + var headers = options.headers.map { HTTPHeaders($0) } ?? HTTPHeaders() + + headers["x-upsert"] = "\(options.upsert)" headers["duplex"] = options.duplex if let metadata = options.metadata { From 1f01ff81b6738bea6c7649b68ef9a1f906d5f271 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 27 Aug 2024 05:31:34 -0300 Subject: [PATCH 07/10] comment out info method --- Sources/Storage/StorageFileApi.swift | 18 ++++++------- .../StorageFileIntegrationTests.swift | 26 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 86f86e8db..1fe5e2efa 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -422,15 +422,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { } /// Retrieves the details of an existing file. - public func info(path: String) async throws -> FileObjectV2 { - try await execute( - HTTPRequest( - url: configuration.url.appendingPathComponent("object/info/public/\(bucketId)/\(path)"), - method: .get - ) - ) - .decoded(decoder: configuration.decoder) - } +// public func info(path: String) async throws -> FileObjectV2 { +// try await execute( +// HTTPRequest( +// url: configuration.url.appendingPathComponent("object/info/public/\(bucketId)/\(path)"), +// method: .get +// ) +// ) +// .decoded(decoder: configuration.decoder) +// } /// Checks the existence of file. public func exists(path: String) async throws -> Bool { diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index f2ae953ec..5816696bc 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -329,19 +329,19 @@ final class StorageFileIntegrationTests: XCTestCase { } } - func testInfo() async throws { - try await storage.from(bucketName).upload( - uploadPath, - data: file, - options: FileOptions( - metadata: ["value": 42] - ) - ) - - let info = try await storage.from(bucketName).info(path: uploadPath) - XCTAssertEqual(info.name, uploadPath) - XCTAssertEqual(info.metadata, ["value": 42]) - } +// func testInfo() async throws { +// try await storage.from(bucketName).upload( +// uploadPath, +// data: file, +// options: FileOptions( +// metadata: ["value": 42] +// ) +// ) +// +// let info = try await storage.from(bucketName).info(path: uploadPath) +// XCTAssertEqual(info.name, uploadPath) +// XCTAssertEqual(info.metadata, ["value": 42]) +// } func testExists() async throws { try await storage.from(bucketName).upload(uploadPath, data: file) From f6af2536dcabe50ef1fa83f4fa5c956be474637e Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 27 Aug 2024 08:07:13 -0300 Subject: [PATCH 08/10] info method calls authenticated endpoint --- Sources/Storage/StorageFileApi.swift | 18 ++++++------- .../StorageFileIntegrationTests.swift | 26 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 1fe5e2efa..c0dfc6873 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -422,15 +422,15 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { } /// Retrieves the details of an existing file. -// public func info(path: String) async throws -> FileObjectV2 { -// try await execute( -// HTTPRequest( -// url: configuration.url.appendingPathComponent("object/info/public/\(bucketId)/\(path)"), -// method: .get -// ) -// ) -// .decoded(decoder: configuration.decoder) -// } + public func info(path: String) async throws -> FileObjectV2 { + try await execute( + HTTPRequest( + url: configuration.url.appendingPathComponent("object/info/authenticated/\(bucketId)/\(path)"), + method: .get + ) + ) + .decoded(decoder: configuration.decoder) + } /// Checks the existence of file. public func exists(path: String) async throws -> Bool { diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift index 5816696bc..f2ae953ec 100644 --- a/Tests/IntegrationTests/StorageFileIntegrationTests.swift +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -329,19 +329,19 @@ final class StorageFileIntegrationTests: XCTestCase { } } -// func testInfo() async throws { -// try await storage.from(bucketName).upload( -// uploadPath, -// data: file, -// options: FileOptions( -// metadata: ["value": 42] -// ) -// ) -// -// let info = try await storage.from(bucketName).info(path: uploadPath) -// XCTAssertEqual(info.name, uploadPath) -// XCTAssertEqual(info.metadata, ["value": 42]) -// } + func testInfo() async throws { + try await storage.from(bucketName).upload( + uploadPath, + data: file, + options: FileOptions( + metadata: ["value": 42] + ) + ) + + let info = try await storage.from(bucketName).info(path: uploadPath) + XCTAssertEqual(info.name, uploadPath) + XCTAssertEqual(info.metadata, ["value": 42]) + } func testExists() async throws { try await storage.from(bucketName).upload(uploadPath, data: file) From 5d85aeaf8f3ff460eab8dbdad86657e6991d103e Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 24 Sep 2024 09:18:03 -0300 Subject: [PATCH 09/10] clean path --- Sources/Storage/StorageFileApi.swift | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index c0dfc6873..5022a9d1a 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -75,9 +75,12 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { let Id: String } + let cleanPath = _removeEmptyFolders(path) + let _path = _getFinalPath(cleanPath) + let response = try await execute( HTTPRequest( - url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), + url: configuration.url.appendingPathComponent("object/\(_path)"), method: method, query: [], formData: formData, @@ -423,9 +426,11 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { /// Retrieves the details of an existing file. public func info(path: String) async throws -> FileObjectV2 { - try await execute( + let _path = _getFinalPath(path) + + return try await execute( HTTPRequest( - url: configuration.url.appendingPathComponent("object/info/authenticated/\(bucketId)/\(path)"), + url: configuration.url.appendingPathComponent("object/info/\(_path)"), method: .get ) ) @@ -652,4 +657,14 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { return SignedURLUploadResponse(path: path, fullPath: fullPath) } + + private func _getFinalPath(_ path: String) -> String { + "\(bucketId)/\(path)" + } + + private func _removeEmptyFolders(_ path: String) -> String { + let trimmedPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let cleanedPath = trimmedPath.replacingOccurrences(of: "/+", with: "/", options: .regularExpression) + return cleanedPath + } } From 433f36b24fc706001a2fcc3684429cdbdb22b408 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 24 Sep 2024 14:36:02 -0300 Subject: [PATCH 10/10] add example for info method call --- Examples/Examples/Storage/FileObjectDetailView.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Examples/Examples/Storage/FileObjectDetailView.swift b/Examples/Examples/Storage/FileObjectDetailView.swift index 8c21faafe..48d483b37 100644 --- a/Examples/Examples/Storage/FileObjectDetailView.swift +++ b/Examples/Examples/Storage/FileObjectDetailView.swift @@ -45,6 +45,15 @@ struct FileObjectDetailView: View { } catch {} } } + + Button("Get info") { + Task { + do { + let info = try await api.info(path: fileObject.name) + lastActionResult = ("info", info) + } catch {} + } + } } if let lastActionResult {