Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 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, 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, 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, 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 (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 (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 (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, 16.0)

'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
Loading