Overview
R2UploadService handles file uploads to Cloudflare R2 storage using the S3-compatible API with AWS Signature V4 authentication. The service provides progress tracking and connection testing capabilities.
Located at: Fiaxe/Services/R2UploadService.swift:5
Type Definition
nonisolated enum R2UploadService
Implemented as a nonisolated enum to allow calling from any actor context without isolation restrictions.
Methods
upload()
Uploads a file to R2 via a signed PUT request with progress tracking.
static func upload(
fileURL: URL,
credentials: R2Credentials,
key: String,
contentType: String,
onProgress: @MainActor @escaping @Sendable (Int64, Int64) -> Void
) async throws -> UploadResult
Security-scoped URL of the file to upload. Must be accessible with proper permissions.
R2 credentials containing:
- Account ID
- Bucket name
- Access key ID
- Secret access key
- Endpoint URL
Object key in the bucket (e.g., "abc12345-photo.jpg"). This becomes the file’s path in R2.
MIME type of the file (e.g., "image/jpeg", "application/pdf"). Used to set the Content-Type header.
onProgress
@MainActor @escaping @Sendable (Int64, Int64) -> Void
required
Progress callback fired on the main actor with (bytesSent, totalBytes). Called periodically during upload to report progress.
Returns: UploadResult containing the HTTP status code and response body.
Throws: URLError or network-related errors if the upload fails.
Implementation Details
The upload process:
- Constructs the full R2 endpoint URL:
{endpoint}/{bucketName}/{key}
- Creates a PUT request with Content-Type and Content-Length headers
- Signs the request using AWS Signature V4 with
UNSIGNED-PAYLOAD (for streaming uploads)
- Uses a custom
URLSessionTaskDelegate to track upload progress
- Returns status code and response body upon completion
// Example from R2UploadService.swift:19-50
let url = await credentials.endpoint
.appendingPathComponent(credentials.bucketName)
.appendingPathComponent(key)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
let fileSize = (try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? Int64) ?? 0
request.setValue("\(fileSize)", forHTTPHeaderField: "Content-Length")
let signedRequest = AWSV4Signer.sign(
request: request,
credentials: credentials,
payloadHash: "UNSIGNED-PAYLOAD"
)
let delegate = UploadProgressDelegate(onProgress: onProgress)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
defer { session.finishTasksAndInvalidate() }
let (data, response) = try await session.upload(for: signedRequest, fromFile: fileURL)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
return UploadResult(httpStatusCode: statusCode, responseBody: data)
testConnection()
Performs a HEAD request to verify connectivity and credentials.
static func testConnection(credentials: R2Credentials) async throws -> Bool
R2 credentials to test. The method verifies access to the specified bucket.
Returns: true if the connection succeeds (HTTP 200), false otherwise.
Throws: Network errors if the request fails.
Implementation Details
// Example from R2UploadService.swift:53-68
let url = await credentials.endpoint.appendingPathComponent(credentials.bucketName)
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
let emptyHash = SHA256.hash(data: Data()).map { String(format: "%02x", $0) }.joined()
let signedRequest = AWSV4Signer.sign(
request: request,
credentials: credentials,
payloadHash: emptyHash
)
let (_, response) = try await URLSession.shared.data(for: signedRequest)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
return statusCode == 200
The method sends a HEAD request to the bucket root to verify:
- Credentials are valid
- Bucket is accessible
- Network connectivity is working
Data Types
UploadResult
Result structure returned from successful uploads.
struct UploadResult: Sendable {
let httpStatusCode: Int
let responseBody: Data
}
HTTP status code from the R2 response. Typically 200 for successful uploads.
Raw response body data from the server. Usually contains XML response with upload details.
Progress Tracking
The service uses a private UploadProgressDelegate class that implements URLSessionTaskDelegate to track upload progress:
private final class UploadProgressDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
private let onProgress: @MainActor @Sendable (Int64, Int64) -> Void
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
let handler = onProgress
let sent = totalBytesSent
let total = totalBytesExpectedToSend
Task { @MainActor in
handler(sent, total)
}
}
}
Progress updates are automatically dispatched to the main actor for UI updates.
Usage Example
// Upload a file with progress tracking
let credentials = R2Credentials(
accountId: "your-account-id",
accessKeyId: "your-access-key",
secretAccessKey: "your-secret-key",
bucketName: "my-bucket"
)
let fileURL = URL(fileURLWithPath: "/path/to/photo.jpg")
let objectKey = "uploads/\(UUID().uuidString)-photo.jpg"
do {
let result = try await R2UploadService.upload(
fileURL: fileURL,
credentials: credentials,
key: objectKey,
contentType: "image/jpeg",
onProgress: { bytesSent, totalBytes in
let progress = Double(bytesSent) / Double(totalBytes)
print("Upload progress: \(Int(progress * 100))%")
}
)
if result.httpStatusCode == 200 {
print("Upload successful!")
}
} catch {
print("Upload failed: \(error)")
}
// Test connection before uploading
let isConnected = try await R2UploadService.testConnection(credentials: credentials)
if isConnected {
print("Connection successful!")
}