field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraBackgroundUploadRequest.swift (5449B)


      1 import Foundation
      2 import RadrootsKit
      3 
      4 /// Exact request translation and matching; no transfer or task ownership.
      5 enum TeraBackgroundUploadRequest {
      6   static let operationInProgress = TeraRuntimeFailure(
      7     schemaVersion: 1, code: "operation_in_progress", category: "operation",
      8     retryable: true, recoveryActions: ["retry_operation_with_same_idempotency_key"],
      9     operationID: "add.media.background", capabilityID: nil,
     10     safeMessage: "A photo upload is already in progress for this draft."
     11   )
     12 
     13   static func prepare(
     14     job: TeraNativeTransferJob,
     15     media: TeraPreparedMedia,
     16     preparer: RadrootsAppleMediaPreparer
     17   ) async throws -> RadrootsBackgroundTransferRequest {
     18     guard job.expectedSHA256 == media.sha256,
     19       job.mediaType == media.mediaType,
     20       job.byteSize == media.byteSize,
     21       let byteSize = Int(exactly: media.byteSize),
     22       let remoteURL = URL(string: job.uploadURL)
     23     else {
     24       throw TeraRuntimeFailure.local(
     25         operation: "add.media.background",
     26         code: "ios.add.background_upload_mismatch",
     27         safeMessage: "The prepared photo no longer matches the authorized upload."
     28       )
     29     }
     30     let identifier = try TeraBackgroundUploadRequest.transferIdentifier(job: job)
     31     let prepared = try RadrootsApplePreparedImage(
     32       file: RadrootsStagedBlobReference(
     33         blobID: media.sha256,
     34         sizeBytes: byteSize,
     35         mediaType: media.mediaType,
     36         filenameHint: "\(media.sha256).png"
     37       ),
     38       sha256: media.sha256,
     39       width: media.width,
     40       height: media.height
     41     )
     42     return try await preparer.blossomUploadRequest(
     43       preparedImage: prepared,
     44       remoteURL: remoteURL,
     45       authorization: job.authorizationHeader,
     46       networkPolicy: remoteURL.scheme?.lowercased() == "https"
     47         ? .publicHTTPS : .simulatorLoopbackHTTP,
     48       identifier: identifier
     49     )
     50   }
     51 
     52   static func transferIdentifier(
     53     job: TeraNativeTransferJob
     54   ) throws -> RadrootsBackgroundTransferIdentifier {
     55     try RadrootsBackgroundTransferIdentifier(
     56       "radroots.add.\(job.ownerID).\(job.expectedRevision).\(job.operationID)"
     57     )
     58   }
     59 
     60   static func persistedRequestMatches(
     61     _ persisted: RadrootsBackgroundTransferRequest,
     62     request: RadrootsBackgroundTransferRequest
     63   ) -> Bool {
     64     persisted.headers.isEmpty && persisted.metadata.isEmpty
     65       && persisted.remoteURL == request.remoteURL
     66       && persisted.method == request.method
     67       && persisted.operation == request.operation
     68       && persisted.networkPolicy == request.networkPolicy
     69       && persisted.responsePolicy == request.responsePolicy
     70       && persisted.expectedSourceSHA256 == request.expectedSourceSHA256
     71       && persisted.maximumTransferBytes == request.maximumTransferBytes
     72   }
     73 
     74   static func persistedRequestMatchesMedia(
     75     _ persisted: RadrootsBackgroundTransferRequest,
     76     media: TeraPreparedMedia,
     77     uploadURL: String?
     78   ) throws -> Bool {
     79     guard let remoteURL = media.remoteURL.flatMap(URL.init(string:)),
     80       let byteSize = Int(exactly: media.byteSize)
     81     else { return false }
     82     let blob = try RadrootsStagedBlobReference(
     83       blobID: media.sha256,
     84       sizeBytes: byteSize,
     85       mediaType: media.mediaType,
     86       filenameHint: "\(media.sha256).png"
     87     )
     88     let responsePolicy = try RadrootsBackgroundTransferResponsePolicy.boundedJSON()
     89     return persisted.headers.isEmpty && persisted.metadata.isEmpty
     90       && (persisted.remoteURL == remoteURL || persisted.remoteURL.absoluteString == uploadURL)
     91       && persisted.method == .put
     92       && persisted.operation == .upload(source: .stagedBlob(blob))
     93       && persisted.networkPolicy
     94         == (remoteURL.scheme?.lowercased() == "https" ? .publicHTTPS : .simulatorLoopbackHTTP)
     95       && persisted.responsePolicy == responsePolicy
     96       && persisted.expectedSourceSHA256 == media.sha256
     97       && persisted.maximumTransferBytes
     98         == RadrootsBackgroundTransferRequest.defaultMaximumTransferBytes
     99   }
    100 
    101   static func replacingIdentifier(
    102     in request: RadrootsBackgroundTransferRequest,
    103     with identifier: RadrootsBackgroundTransferIdentifier
    104   ) throws -> RadrootsBackgroundTransferRequest {
    105     try RadrootsBackgroundTransferRequest(
    106       identifier: identifier,
    107       remoteURL: request.remoteURL,
    108       method: request.method,
    109       operation: request.operation,
    110       headers: request.headers,
    111       metadata: request.metadata,
    112       networkPolicy: request.networkPolicy,
    113       responsePolicy: request.responsePolicy,
    114       expectedSourceSHA256: request.expectedSourceSHA256,
    115       maximumTransferBytes: request.maximumTransferBytes
    116     )
    117   }
    118 
    119   static func transferIdentity(
    120     _ identifier: RadrootsBackgroundTransferIdentifier
    121   ) -> TeraNativeTransferIdentity? {
    122     let components = identifier.rawValue.split(separator: ".", omittingEmptySubsequences: false)
    123     guard components.count == 5,
    124       components[0] == "radroots",
    125       components[1] == "add",
    126       components[2].range(of: "^[0-9a-f]{32}$", options: .regularExpression) != nil,
    127       let revision = UInt64(components[3]),
    128       String(revision) == components[3],
    129       components[4].range(of: "^[0-9a-f]{32}$", options: .regularExpression) != nil
    130     else { return nil }
    131     return TeraNativeTransferIdentity(draftID: String(components[2]), revision: revision, attempt: String(components[4]))
    132   }
    133 }
    134 
    135 struct TeraNativeTransferIdentity: Sendable, Equatable {
    136   let draftID: String
    137   let revision: UInt64
    138   let attempt: String
    139 }