Skip to content

Commit d9ba673

Browse files
authored
feat(storage): add info, exists, custom metadata, and methods for uploading file URL (#510)
1 parent 6522826 commit d9ba673

File tree

13 files changed

+496
-163
lines changed

13 files changed

+496
-163
lines changed

.swiftpm/configuration/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/Examples/Storage/FileObjectDetailView.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ struct FileObjectDetailView: View {
4545
} catch {}
4646
}
4747
}
48+
49+
Button("Get info") {
50+
Task {
51+
do {
52+
let info = try await api.info(path: fileObject.name)
53+
lastActionResult = ("info", info)
54+
} catch {}
55+
}
56+
}
4857
}
4958

5059
if let lastActionResult {

Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let package = Package(
2222
.library(name: "Supabase", targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"]),
2323
],
2424
dependencies: [
25+
.package(url: "https://github.com/grdsdev/MultipartFormData", from: "0.1.0"),
2526
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"),
2627
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"),
2728
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
@@ -127,7 +128,13 @@ let package = Package(
127128
"TestHelpers",
128129
]
129130
),
130-
.target(name: "Storage", dependencies: ["Helpers"]),
131+
.target(
132+
name: "Storage",
133+
dependencies: [
134+
"MultipartFormData",
135+
"Helpers",
136+
]
137+
),
131138
.testTarget(
132139
name: "StorageTests",
133140
dependencies: [

Sources/Storage/Deprecated.swift

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,109 @@ extension StorageFileApi {
6565
) async throws -> String {
6666
try await uploadToSignedURL(path: path, token: token, file: file, options: options).fullPath
6767
}
68+
69+
@available(*, deprecated, renamed: "upload(_:data:options:)")
70+
@discardableResult
71+
public func upload(
72+
path: String,
73+
file: Data,
74+
options: FileOptions = FileOptions()
75+
) async throws -> FileUploadResponse {
76+
try await upload(path, data: file, options: options)
77+
}
78+
79+
@available(*, deprecated, renamed: "update(_:data:options:)")
80+
@discardableResult
81+
public func update(
82+
path: String,
83+
file: Data,
84+
options: FileOptions = FileOptions()
85+
) async throws -> FileUploadResponse {
86+
try await update(path, data: file, options: options)
87+
}
88+
89+
@available(*, deprecated, renamed: "updateToSignedURL(_:token:data:options:)")
90+
@discardableResult
91+
public func uploadToSignedURL(
92+
path: String,
93+
token: String,
94+
file: Data,
95+
options: FileOptions = FileOptions()
96+
) async throws -> SignedURLUploadResponse {
97+
try await uploadToSignedURL(path, token: token, data: file, options: options)
98+
}
99+
}
100+
101+
@available(
102+
*,
103+
deprecated,
104+
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."
105+
)
106+
public struct File: Hashable, Equatable {
107+
public var name: String
108+
public var data: Data
109+
public var fileName: String?
110+
public var contentType: String?
111+
112+
public init(name: String, data: Data, fileName: String?, contentType: String?) {
113+
self.name = name
114+
self.data = data
115+
self.fileName = fileName
116+
self.contentType = contentType
117+
}
118+
}
119+
120+
@available(
121+
*,
122+
deprecated,
123+
renamed: "MultipartFormData",
124+
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."
125+
)
126+
public class FormData {
127+
var files: [File] = []
128+
var boundary: String
129+
130+
public init(boundary: String = UUID().uuidString) {
131+
self.boundary = boundary
132+
}
133+
134+
public func append(file: File) {
135+
files.append(file)
136+
}
137+
138+
public var contentType: String {
139+
"multipart/form-data; boundary=\(boundary)"
140+
}
141+
142+
public var data: Data {
143+
var data = Data()
144+
145+
for file in files {
146+
data.append("--\(boundary)\r\n")
147+
data.append("Content-Disposition: form-data; name=\"\(file.name)\"")
148+
if let filename = file.fileName?.replacingOccurrences(of: "\"", with: "_") {
149+
data.append("; filename=\"\(filename)\"")
150+
}
151+
data.append("\r\n")
152+
if let contentType = file.contentType {
153+
data.append("Content-Type: \(contentType)\r\n")
154+
}
155+
data.append("\r\n")
156+
data.append(file.data)
157+
data.append("\r\n")
158+
}
159+
160+
data.append("--\(boundary)--\r\n")
161+
return data
162+
}
163+
}
164+
165+
extension Data {
166+
mutating func append(_ string: String) {
167+
let data = string.data(
168+
using: String.Encoding.utf8,
169+
allowLossyConversion: true
170+
)
171+
append(data!)
172+
}
68173
}

Sources/Storage/Helpers.swift

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,67 @@
77

88
import Foundation
99

10-
#if canImport(CoreServices)
10+
#if canImport(MobileCoreServices)
11+
import MobileCoreServices
12+
#elseif canImport(CoreServices)
1113
import CoreServices
1214
#endif
1315

1416
#if canImport(UniformTypeIdentifiers)
1517
import UniformTypeIdentifiers
16-
#endif
1718

18-
#if os(Linux) || os(Windows)
19-
/// On Linux or Windows this method always returns `application/octet-stream`.
20-
func mimeTypeForExtension(_: String) -> String {
21-
"application/octet-stream"
19+
func mimeType(forPathExtension pathExtension: String) -> String {
20+
#if swift(>=5.9)
21+
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) {
22+
return UTType(filenameExtension: pathExtension)?.preferredMIMEType
23+
?? "application/octet-stream"
24+
} else {
25+
if let id = UTTypeCreatePreferredIdentifierForTag(
26+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
27+
)?.takeRetainedValue(),
28+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
29+
.takeRetainedValue()
30+
{
31+
return contentType as String
32+
}
33+
34+
return "application/octet-stream"
35+
}
36+
#else
37+
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
38+
return UTType(filenameExtension: pathExtension)?.preferredMIMEType
39+
?? "application/octet-stream"
40+
} else {
41+
if let id = UTTypeCreatePreferredIdentifierForTag(
42+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
43+
)?.takeRetainedValue(),
44+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
45+
.takeRetainedValue()
46+
{
47+
return contentType as String
48+
}
49+
50+
return "application/octet-stream"
51+
}
52+
#endif
2253
}
2354
#else
24-
func mimeTypeForExtension(_ fileExtension: String) -> String {
25-
if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, visionOS 1.0, *) {
26-
return UTType(filenameExtension: fileExtension)?.preferredMIMEType ?? "application/octet-stream"
27-
} else {
28-
guard
29-
let type = UTTypeCreatePreferredIdentifierForTag(
30-
kUTTagClassFilenameExtension,
31-
fileExtension as NSString,
32-
nil
33-
)?.takeUnretainedValue(),
34-
let mimeType = UTTypeCopyPreferredTagWithClass(
35-
type,
36-
kUTTagClassMIMEType
37-
)?.takeUnretainedValue()
38-
else { return "application/octet-stream" }
39-
40-
return mimeType as String
41-
}
55+
56+
// MARK: - Private - Mime Type
57+
58+
func mimeType(forPathExtension pathExtension: String) -> String {
59+
#if canImport(CoreServices) || canImport(MobileCoreServices)
60+
if let id = UTTypeCreatePreferredIdentifierForTag(
61+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
62+
)?.takeRetainedValue(),
63+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
64+
.takeRetainedValue()
65+
{
66+
return contentType as String
67+
}
68+
#endif
69+
70+
return "application/octet-stream"
4271
}
4372
#endif
4473

Sources/Storage/MultipartFile.swift

Lines changed: 0 additions & 64 deletions
This file was deleted.

Sources/Storage/StorageApi.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Helpers
3+
import class MultipartFormData.MultipartFormData
34

45
#if canImport(FoundationNetworking)
56
import FoundationNetworking
@@ -36,7 +37,10 @@ public class StorageApi: @unchecked Sendable {
3637
let response = try await http.send(request)
3738

3839
guard (200 ..< 300).contains(response.statusCode) else {
39-
if let error = try? configuration.decoder.decode(StorageError.self, from: response.data) {
40+
if let error = try? configuration.decoder.decode(
41+
StorageError.self,
42+
from: response.data
43+
) {
4044
throw error
4145
}
4246

@@ -52,23 +56,23 @@ extension HTTPRequest {
5256
url: URL,
5357
method: HTTPMethod,
5458
query: [URLQueryItem],
55-
formData: FormData,
59+
formData: MultipartFormData,
5660
options: FileOptions,
5761
headers: HTTPHeaders = [:]
58-
) {
62+
) throws {
5963
var headers = headers
6064
if headers["Content-Type"] == nil {
6165
headers["Content-Type"] = formData.contentType
6266
}
6367
if headers["Cache-Control"] == nil {
6468
headers["Cache-Control"] = "max-age=\(options.cacheControl)"
6569
}
66-
self.init(
70+
try self.init(
6771
url: url,
6872
method: method,
6973
query: query,
7074
headers: headers,
71-
body: formData.data
75+
body: formData.encode()
7276
)
7377
}
7478
}

Sources/Storage/StorageBucketApi.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Helpers
66
#endif
77

88
/// Storage Bucket API
9-
public class StorageBucketApi: StorageApi {
9+
public class StorageBucketApi: StorageApi, @unchecked Sendable {
1010
/// Retrieves the details of all Storage buckets within an existing product.
1111
public func listBuckets() async throws -> [Bucket] {
1212
try await execute(

0 commit comments

Comments
 (0)