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 }