Skip to content

Commit

Permalink
fix(storage): cache control (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev authored Oct 23, 2024
1 parent 2b70fea commit 8a2b196
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 176 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"Supabase",
"whitespaces",
"xctest"
]
],
"makefile.configureOnOpen": false
}
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ let package = Package(
name: "StorageTests",
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
"Storage",
]
Expand Down
83 changes: 57 additions & 26 deletions Sources/Helpers/URLSession+AsyncAwait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,54 @@
) async throws -> (Data, URLResponse) {
let helper = URLSessionTaskCancellationHelper()

return try await withTaskCancellationHandler(operation: {
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
let task = dataTask(
with: request,
completionHandler: { data, response, error in
if let error {
continuation.resume(throwing: error)
} else if let data, let response {
continuation.resume(returning: (data, response))
} else {
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
}
})

helper.register(task)

task.resume()
}
},
onCancel: {
helper.cancel()
})
}

public func data(
from url: URL,
delegate _: (any URLSessionTaskDelegate)? = nil
) async throws -> (Data, URLResponse) {
let helper = URLSessionTaskCancellationHelper()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
let task = dataTask(with: request, completionHandler: { data, response, error in
let task = dataTask(with: url) { data, response, error in
if let error {
continuation.resume(throwing: error)
} else if let data, let response {
continuation.resume(returning: (data, response))
} else {
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
}
})
}

helper.register(task)

task.resume()
}
}, onCancel: {
} onCancel: {
helper.cancel()
})
}
}

public func upload(
Expand All @@ -105,29 +134,31 @@
) async throws -> (Data, URLResponse) {
let helper = URLSessionTaskCancellationHelper()

return try await withTaskCancellationHandler(operation: {
try await withCheckedThrowingContinuation { continuation in
let task = uploadTask(
with: request,
from: bodyData,
completionHandler: { data, response, error in
if let error {
continuation.resume(throwing: error)
} else if let data, let response {
continuation.resume(returning: (data, response))
} else {
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
let task = uploadTask(
with: request,
from: bodyData,
completionHandler: { data, response, error in
if let error {
continuation.resume(throwing: error)
} else if let data, let response {
continuation.resume(returning: (data, response))
} else {
continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned)
}
}
}
)
)

helper.register(task)
helper.register(task)

task.resume()
}
}, onCancel: {
helper.cancel()
})
task.resume()
}
},
onCancel: {
helper.cancel()
})
}
}

Expand Down
70 changes: 70 additions & 0 deletions Sources/Storage/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,76 @@
//

import Foundation
import Helpers

#if canImport(MobileCoreServices)
import MobileCoreServices
#elseif canImport(CoreServices)
import CoreServices
#endif

#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers

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

// 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

func encodeMetadata(_ metadata: JSONObject) -> Data {
let encoder = AnyJSON.encoder

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (IOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (IOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (IOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MAC_CATALYST, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MAC_CATALYST, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (IOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (IOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / Integration Tests

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MACOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MACOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MACOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MACOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MAC_CATALYST, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MAC_CATALYST, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (MAC_CATALYST, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, IOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MAC_CATALYST, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, MACOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, TVOS, 16.0)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (WATCHOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().

Check warning on line 76 in Sources/Storage/Helpers.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (test, WATCHOS, 15.4)

'encoder' is deprecated: encoder is deprecated, AnyJSON now uses default JSONEncoder().
return (try? encoder.encode(metadata)) ?? "{}".data(using: .utf8)!
}

extension String {
var pathExtension: String {
Expand Down
84 changes: 43 additions & 41 deletions Sources/Storage/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
//

import Foundation
import Helpers
import HTTPTypes
import Helpers

#if canImport(MobileCoreServices)
import MobileCoreServices
Expand Down Expand Up @@ -59,21 +59,22 @@ class MultipartFormData {
}

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

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

static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
let boundaryText = switch boundaryType {
case .initial:
"--\(boundary)\(EncodingCharacters.crlf)"
case .encapsulated:
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
case .final:
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
}
let boundaryText =
switch boundaryType {
case .initial:
"--\(boundary)\(EncodingCharacters.crlf)"
case .encapsulated:
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
case .final:
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
}

return Data(boundaryText.utf8)
}
Expand All @@ -96,7 +97,7 @@ class MultipartFormData {
// MARK: - Properties

/// Default memory threshold used when encoding `MultipartFormData`, in bytes.
static let encodingMemoryThreshold: UInt64 = 10000000
static let encodingMemoryThreshold: UInt64 = 10_000_000

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

return Data(headerText.utf8)
}
Expand Down Expand Up @@ -481,7 +482,7 @@ class MultipartFormData {

if bytesRead > 0 {
if buffer.count != bytesRead {
buffer = Array(buffer[0 ..< bytesRead])
buffer = Array(buffer[0..<bytesRead])
}

try write(&buffer, to: outputStream)
Expand All @@ -492,7 +493,8 @@ class MultipartFormData {
}
}

private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws
{
if bodyPart.hasFinalBoundary {
try write(finalBoundaryData(), to: outputStream)
}
Expand Down Expand Up @@ -520,7 +522,7 @@ class MultipartFormData {
bytesToWrite -= bytesWritten

if bytesToWrite > 0 {
buffer = Array(buffer[bytesWritten ..< buffer.count])
buffer = Array(buffer[bytesWritten..<buffer.count])
}
}
}
Expand Down Expand Up @@ -577,7 +579,7 @@ class MultipartFormData {
kUTTagClassFilenameExtension, pathExtension as CFString, nil
)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
.takeRetainedValue()
.takeRetainedValue()
{
return contentType as String
}
Expand All @@ -593,7 +595,7 @@ class MultipartFormData {
kUTTagClassFilenameExtension, pathExtension as CFString, nil
)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
.takeRetainedValue()
.takeRetainedValue()
{
return contentType as String
}
Expand All @@ -615,7 +617,7 @@ class MultipartFormData {
kUTTagClassFilenameExtension, pathExtension as CFString, nil
)?.takeRetainedValue(),
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
.takeRetainedValue()
.takeRetainedValue()
{
return contentType as String
}
Expand Down Expand Up @@ -650,37 +652,37 @@ enum MultipartFormDataError: Error {
var underlyingError: (any Error)? {
switch self {
case let .bodyPartFileNotReachableWithError(_, error),
let .bodyPartFileSizeQueryFailedWithError(_, error),
let .inputStreamReadFailed(error),
let .outputStreamWriteFailed(error):
let .bodyPartFileSizeQueryFailedWithError(_, error),
let .inputStreamReadFailed(error),
let .outputStreamWriteFailed(error):
error

case .bodyPartURLInvalid,
.bodyPartFilenameInvalid,
.bodyPartFileNotReachable,
.bodyPartFileIsDirectory,
.bodyPartFileSizeNotAvailable,
.bodyPartInputStreamCreationFailed,
.outputStreamFileAlreadyExists,
.outputStreamURLInvalid,
.outputStreamCreationFailed:
.bodyPartFilenameInvalid,
.bodyPartFileNotReachable,
.bodyPartFileIsDirectory,
.bodyPartFileSizeNotAvailable,
.bodyPartInputStreamCreationFailed,
.outputStreamFileAlreadyExists,
.outputStreamURLInvalid,
.outputStreamCreationFailed:
nil
}
}

var url: URL? {
switch self {
case let .bodyPartURLInvalid(url),
let .bodyPartFilenameInvalid(url),
let .bodyPartFileNotReachable(url),
let .bodyPartFileNotReachableWithError(url, _),
let .bodyPartFileIsDirectory(url),
let .bodyPartFileSizeNotAvailable(url),
let .bodyPartFileSizeQueryFailedWithError(url, _),
let .bodyPartInputStreamCreationFailed(url),
let .outputStreamFileAlreadyExists(url),
let .outputStreamURLInvalid(url),
let .outputStreamCreationFailed(url):
let .bodyPartFilenameInvalid(url),
let .bodyPartFileNotReachable(url),
let .bodyPartFileNotReachableWithError(url, _),
let .bodyPartFileIsDirectory(url),
let .bodyPartFileSizeNotAvailable(url),
let .bodyPartFileSizeQueryFailedWithError(url, _),
let .bodyPartInputStreamCreationFailed(url),
let .outputStreamFileAlreadyExists(url),
let .outputStreamURLInvalid(url),
let .outputStreamCreationFailed(url):
url

case .inputStreamReadFailed, .outputStreamWriteFailed:
Expand Down
Loading

0 comments on commit 8a2b196

Please sign in to comment.