Skip to content

Commit 8a2b196

Browse files
authored
fix(storage): cache control (supabase#551)
1 parent 2b70fea commit 8a2b196

10 files changed

+455
-176
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
"Supabase",
99
"whitespaces",
1010
"xctest"
11-
]
11+
],
12+
"makefile.configureOnOpen": false
1213
}

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ let package = Package(
144144
name: "StorageTests",
145145
dependencies: [
146146
.product(name: "CustomDump", package: "swift-custom-dump"),
147+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
147148
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
148149
"Storage",
149150
]

Sources/Helpers/URLSession+AsyncAwait.swift

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,54 @@
7777
) async throws -> (Data, URLResponse) {
7878
let helper = URLSessionTaskCancellationHelper()
7979

80-
return try await withTaskCancellationHandler(operation: {
80+
return try await withTaskCancellationHandler(
81+
operation: {
82+
try await withCheckedThrowingContinuation { continuation in
83+
let task = dataTask(
84+
with: request,
85+
completionHandler: { data, response, error in
86+
if let error {
87+
continuation.resume(throwing: error)
88+
} else if let data, let response {
89+
continuation.resume(returning: (data, response))
90+
} else {
91+
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
92+
}
93+
})
94+
95+
helper.register(task)
96+
97+
task.resume()
98+
}
99+
},
100+
onCancel: {
101+
helper.cancel()
102+
})
103+
}
104+
105+
public func data(
106+
from url: URL,
107+
delegate _: (any URLSessionTaskDelegate)? = nil
108+
) async throws -> (Data, URLResponse) {
109+
let helper = URLSessionTaskCancellationHelper()
110+
return try await withTaskCancellationHandler {
81111
try await withCheckedThrowingContinuation { continuation in
82-
let task = dataTask(with: request, completionHandler: { data, response, error in
112+
let task = dataTask(with: url) { data, response, error in
83113
if let error {
84114
continuation.resume(throwing: error)
85115
} else if let data, let response {
86116
continuation.resume(returning: (data, response))
87117
} else {
88118
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
89119
}
90-
})
120+
}
91121

92122
helper.register(task)
93-
94123
task.resume()
95124
}
96-
}, onCancel: {
125+
} onCancel: {
97126
helper.cancel()
98-
})
127+
}
99128
}
100129

101130
public func upload(
@@ -105,29 +134,31 @@
105134
) async throws -> (Data, URLResponse) {
106135
let helper = URLSessionTaskCancellationHelper()
107136

108-
return try await withTaskCancellationHandler(operation: {
109-
try await withCheckedThrowingContinuation { continuation in
110-
let task = uploadTask(
111-
with: request,
112-
from: bodyData,
113-
completionHandler: { data, response, error in
114-
if let error {
115-
continuation.resume(throwing: error)
116-
} else if let data, let response {
117-
continuation.resume(returning: (data, response))
118-
} else {
119-
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
137+
return try await withTaskCancellationHandler(
138+
operation: {
139+
try await withCheckedThrowingContinuation { continuation in
140+
let task = uploadTask(
141+
with: request,
142+
from: bodyData,
143+
completionHandler: { data, response, error in
144+
if let error {
145+
continuation.resume(throwing: error)
146+
} else if let data, let response {
147+
continuation.resume(returning: (data, response))
148+
} else {
149+
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
150+
}
120151
}
121-
}
122-
)
152+
)
123153

124-
helper.register(task)
154+
helper.register(task)
125155

126-
task.resume()
127-
}
128-
}, onCancel: {
129-
helper.cancel()
130-
})
156+
task.resume()
157+
}
158+
},
159+
onCancel: {
160+
helper.cancel()
161+
})
131162
}
132163
}
133164

Sources/Storage/Helpers.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,76 @@
66
//
77

88
import Foundation
9+
import Helpers
10+
11+
#if canImport(MobileCoreServices)
12+
import MobileCoreServices
13+
#elseif canImport(CoreServices)
14+
import CoreServices
15+
#endif
16+
17+
#if canImport(UniformTypeIdentifiers)
18+
import UniformTypeIdentifiers
19+
20+
func mimeType(forPathExtension pathExtension: String) -> String {
21+
#if swift(>=5.9)
22+
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) {
23+
return UTType(filenameExtension: pathExtension)?.preferredMIMEType
24+
?? "application/octet-stream"
25+
} else {
26+
if let id = UTTypeCreatePreferredIdentifierForTag(
27+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
28+
)?.takeRetainedValue(),
29+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
30+
.takeRetainedValue()
31+
{
32+
return contentType as String
33+
}
34+
35+
return "application/octet-stream"
36+
}
37+
#else
38+
if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
39+
return UTType(filenameExtension: pathExtension)?.preferredMIMEType
40+
?? "application/octet-stream"
41+
} else {
42+
if let id = UTTypeCreatePreferredIdentifierForTag(
43+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
44+
)?.takeRetainedValue(),
45+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
46+
.takeRetainedValue()
47+
{
48+
return contentType as String
49+
}
50+
51+
return "application/octet-stream"
52+
}
53+
#endif
54+
}
55+
#else
56+
57+
// MARK: - Private - Mime Type
58+
59+
func mimeType(forPathExtension pathExtension: String) -> String {
60+
#if canImport(CoreServices) || canImport(MobileCoreServices)
61+
if let id = UTTypeCreatePreferredIdentifierForTag(
62+
kUTTagClassFilenameExtension, pathExtension as CFString, nil
63+
)?.takeRetainedValue(),
64+
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
65+
.takeRetainedValue()
66+
{
67+
return contentType as String
68+
}
69+
#endif
70+
71+
return "application/octet-stream"
72+
}
73+
#endif
74+
75+
func encodeMetadata(_ metadata: JSONObject) -> Data {
76+
let encoder = AnyJSON.encoder
77+
return (try? encoder.encode(metadata)) ?? "{}".data(using: .utf8)!
78+
}
979

1080
extension String {
1181
var pathExtension: String {

Sources/Storage/MultipartFormData.swift

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
//
2626

2727
import Foundation
28-
import Helpers
2928
import HTTPTypes
29+
import Helpers
3030

3131
#if canImport(MobileCoreServices)
3232
import MobileCoreServices
@@ -59,21 +59,22 @@ class MultipartFormData {
5959
}
6060

6161
static func randomBoundary() -> String {
62-
let first = UInt32.random(in: UInt32.min ... UInt32.max)
63-
let second = UInt32.random(in: UInt32.min ... UInt32.max)
62+
let first = UInt32.random(in: UInt32.min...UInt32.max)
63+
let second = UInt32.random(in: UInt32.min...UInt32.max)
6464

6565
return String(format: "alamofire.boundary.%08x%08x", first, second)
6666
}
6767

6868
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
69-
let boundaryText = switch boundaryType {
70-
case .initial:
71-
"--\(boundary)\(EncodingCharacters.crlf)"
72-
case .encapsulated:
73-
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
74-
case .final:
75-
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
76-
}
69+
let boundaryText =
70+
switch boundaryType {
71+
case .initial:
72+
"--\(boundary)\(EncodingCharacters.crlf)"
73+
case .encapsulated:
74+
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
75+
case .final:
76+
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
77+
}
7778

7879
return Data(boundaryText.utf8)
7980
}
@@ -96,7 +97,7 @@ class MultipartFormData {
9697
// MARK: - Properties
9798

9899
/// Default memory threshold used when encoding `MultipartFormData`, in bytes.
99-
static let encodingMemoryThreshold: UInt64 = 10000000
100+
static let encodingMemoryThreshold: UInt64 = 10_000_000
100101

101102
/// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
102103
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
@@ -402,8 +403,8 @@ class MultipartFormData {
402403
private func encodeHeaders(for bodyPart: BodyPart) -> Data {
403404
let headerText =
404405
bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
405-
.joined()
406-
+ EncodingCharacters.crlf
406+
.joined()
407+
+ EncodingCharacters.crlf
407408

408409
return Data(headerText.utf8)
409410
}
@@ -481,7 +482,7 @@ class MultipartFormData {
481482

482483
if bytesRead > 0 {
483484
if buffer.count != bytesRead {
484-
buffer = Array(buffer[0 ..< bytesRead])
485+
buffer = Array(buffer[0..<bytesRead])
485486
}
486487

487488
try write(&buffer, to: outputStream)
@@ -492,7 +493,8 @@ class MultipartFormData {
492493
}
493494
}
494495

495-
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
496+
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws
497+
{
496498
if bodyPart.hasFinalBoundary {
497499
try write(finalBoundaryData(), to: outputStream)
498500
}
@@ -520,7 +522,7 @@ class MultipartFormData {
520522
bytesToWrite -= bytesWritten
521523

522524
if bytesToWrite > 0 {
523-
buffer = Array(buffer[bytesWritten ..< buffer.count])
525+
buffer = Array(buffer[bytesWritten..<buffer.count])
524526
}
525527
}
526528
}
@@ -577,7 +579,7 @@ class MultipartFormData {
577579
kUTTagClassFilenameExtension, pathExtension as CFString, nil
578580
)?.takeRetainedValue(),
579581
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
580-
.takeRetainedValue()
582+
.takeRetainedValue()
581583
{
582584
return contentType as String
583585
}
@@ -593,7 +595,7 @@ class MultipartFormData {
593595
kUTTagClassFilenameExtension, pathExtension as CFString, nil
594596
)?.takeRetainedValue(),
595597
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
596-
.takeRetainedValue()
598+
.takeRetainedValue()
597599
{
598600
return contentType as String
599601
}
@@ -615,7 +617,7 @@ class MultipartFormData {
615617
kUTTagClassFilenameExtension, pathExtension as CFString, nil
616618
)?.takeRetainedValue(),
617619
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
618-
.takeRetainedValue()
620+
.takeRetainedValue()
619621
{
620622
return contentType as String
621623
}
@@ -650,37 +652,37 @@ enum MultipartFormDataError: Error {
650652
var underlyingError: (any Error)? {
651653
switch self {
652654
case let .bodyPartFileNotReachableWithError(_, error),
653-
let .bodyPartFileSizeQueryFailedWithError(_, error),
654-
let .inputStreamReadFailed(error),
655-
let .outputStreamWriteFailed(error):
655+
let .bodyPartFileSizeQueryFailedWithError(_, error),
656+
let .inputStreamReadFailed(error),
657+
let .outputStreamWriteFailed(error):
656658
error
657659

658660
case .bodyPartURLInvalid,
659-
.bodyPartFilenameInvalid,
660-
.bodyPartFileNotReachable,
661-
.bodyPartFileIsDirectory,
662-
.bodyPartFileSizeNotAvailable,
663-
.bodyPartInputStreamCreationFailed,
664-
.outputStreamFileAlreadyExists,
665-
.outputStreamURLInvalid,
666-
.outputStreamCreationFailed:
661+
.bodyPartFilenameInvalid,
662+
.bodyPartFileNotReachable,
663+
.bodyPartFileIsDirectory,
664+
.bodyPartFileSizeNotAvailable,
665+
.bodyPartInputStreamCreationFailed,
666+
.outputStreamFileAlreadyExists,
667+
.outputStreamURLInvalid,
668+
.outputStreamCreationFailed:
667669
nil
668670
}
669671
}
670672

671673
var url: URL? {
672674
switch self {
673675
case let .bodyPartURLInvalid(url),
674-
let .bodyPartFilenameInvalid(url),
675-
let .bodyPartFileNotReachable(url),
676-
let .bodyPartFileNotReachableWithError(url, _),
677-
let .bodyPartFileIsDirectory(url),
678-
let .bodyPartFileSizeNotAvailable(url),
679-
let .bodyPartFileSizeQueryFailedWithError(url, _),
680-
let .bodyPartInputStreamCreationFailed(url),
681-
let .outputStreamFileAlreadyExists(url),
682-
let .outputStreamURLInvalid(url),
683-
let .outputStreamCreationFailed(url):
676+
let .bodyPartFilenameInvalid(url),
677+
let .bodyPartFileNotReachable(url),
678+
let .bodyPartFileNotReachableWithError(url, _),
679+
let .bodyPartFileIsDirectory(url),
680+
let .bodyPartFileSizeNotAvailable(url),
681+
let .bodyPartFileSizeQueryFailedWithError(url, _),
682+
let .bodyPartInputStreamCreationFailed(url),
683+
let .outputStreamFileAlreadyExists(url),
684+
let .outputStreamURLInvalid(url),
685+
let .outputStreamCreationFailed(url):
684686
url
685687

686688
case .inputStreamReadFailed, .outputStreamWriteFailed:

0 commit comments

Comments
 (0)