Skip to main content

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
fileURL
URL
required
Security-scoped URL of the file to upload. Must be accessible with proper permissions.
credentials
R2Credentials
required
R2 credentials containing:
  • Account ID
  • Bucket name
  • Access key ID
  • Secret access key
  • Endpoint URL
key
String
required
Object key in the bucket (e.g., "abc12345-photo.jpg"). This becomes the file’s path in R2.
contentType
String
required
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:
  1. Constructs the full R2 endpoint URL: {endpoint}/{bucketName}/{key}
  2. Creates a PUT request with Content-Type and Content-Length headers
  3. Signs the request using AWS Signature V4 with UNSIGNED-PAYLOAD (for streaming uploads)
  4. Uses a custom URLSessionTaskDelegate to track upload progress
  5. 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
credentials
R2Credentials
required
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
}
httpStatusCode
Int
HTTP status code from the R2 response. Typically 200 for successful uploads.
responseBody
Data
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!")
}