RadrootsBackgroundTransferSnapshot.swift (6486B)
1 import Foundation 2 3 public struct RadrootsBackgroundTransferSnapshot: Sendable, Equatable, Hashable, Codable, 4 CustomDebugStringConvertible { 5 public let identifier: RadrootsBackgroundTransferIdentifier 6 public let request: RadrootsBackgroundTransferRequest 7 public let state: RadrootsBackgroundTransferState 8 public let progress: RadrootsBackgroundTransferProgress 9 public let failure: RadrootsBackgroundTransferFailure? 10 public let response: RadrootsBackgroundTransferResponse? 11 public let downloadedArtifact: RadrootsBackgroundDownloadedArtifact? 12 public let possibleRemoteOrphan: Bool 13 public let updatedAt: Date 14 public let executionID: UUID? 15 public let uploadLease: RadrootsStagedBlobReference? 16 17 public init( 18 request: RadrootsBackgroundTransferRequest, state: RadrootsBackgroundTransferState = .queued, 19 progress: RadrootsBackgroundTransferProgress = .zero, 20 failure: RadrootsBackgroundTransferFailure? = nil, 21 response: RadrootsBackgroundTransferResponse? = nil, 22 downloadedArtifact: RadrootsBackgroundDownloadedArtifact? = nil, 23 possibleRemoteOrphan: Bool = false, updatedAt: Date = Date(), 24 executionID: UUID? = nil, uploadLease: RadrootsStagedBlobReference? = nil 25 ) throws { 26 guard updatedAt.timeIntervalSinceReferenceDate.isFinite else { 27 throw RadrootsBackgroundTransferError.invalidRequest 28 } 29 if let uploadLease { 30 guard executionID != nil, request.isUpload, 31 uploadLease.blobID.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil, 32 uploadLease.sizeBytes >= 0, UInt64(uploadLease.sizeBytes) <= request.maximumTransferBytes, 33 request.expectedSourceSHA256 == nil || request.expectedSourceSHA256 == uploadLease.blobID 34 else { throw RadrootsBackgroundTransferError.invalidRequest } 35 } 36 guard response == nil || state == .awaitingVerification || state == .completed else { 37 throw RadrootsBackgroundTransferError.invalidRequest 38 } 39 if let response { 40 try RadrootsBackgroundTransferValidation.validate( 41 response: response, policy: request.responsePolicy 42 ) 43 } 44 if possibleRemoteOrphan { 45 guard case .upload = request.operation, 46 state == .failed || state == .interrupted || state == .cancelled || state == .expired 47 else { 48 throw RadrootsBackgroundTransferError.invalidRequest 49 } 50 } 51 if let downloadedArtifact { 52 guard case let .download(destination) = request.operation, 53 destination == .file(downloadedArtifact.file), 54 state == .awaitingVerification || state == .completed 55 else { 56 throw RadrootsBackgroundTransferError.invalidRequest 57 } 58 } 59 identifier = request.identifier 60 self.request = request 61 self.state = state 62 self.progress = progress 63 self.failure = failure 64 self.response = response 65 self.downloadedArtifact = downloadedArtifact 66 self.possibleRemoteOrphan = possibleRemoteOrphan 67 self.updatedAt = updatedAt 68 self.executionID = executionID 69 self.uploadLease = uploadLease 70 } 71 72 public var debugDescription: String { 73 let statusCode = response?.statusCode.description ?? "none" 74 return "RadrootsBackgroundTransferSnapshot(identifier: \(identifier.rawValue), state: \(state.rawValue), " 75 + "bytesTransferred: \(progress.bytesTransferred), statusCode: \(statusCode), " 76 + "downloadedArtifact: \(downloadedArtifact == nil ? "none" : "present"), " 77 + "possibleRemoteOrphan: \(possibleRemoteOrphan))" 78 } 79 80 func redactedForPersistence() throws -> Self { 81 try Self( 82 request: request.redactedForPersistence(), state: state, progress: progress, 83 failure: failure, response: response, 84 downloadedArtifact: downloadedArtifact, possibleRemoteOrphan: possibleRemoteOrphan, 85 updatedAt: updatedAt, executionID: executionID, uploadLease: uploadLease 86 ) 87 } 88 89 private enum CodingKeys: String, CodingKey { 90 case request 91 case state 92 case progress 93 case failure 94 case response 95 case downloadedArtifact 96 case possibleRemoteOrphan 97 case updatedAt 98 case executionID 99 case uploadLease 100 } 101 102 public init(from decoder: any Decoder) throws { 103 let values = try decoder.container(keyedBy: CodingKeys.self) 104 try self.init( 105 request: values.decode(RadrootsBackgroundTransferRequest.self, forKey: .request), 106 state: values.decode(RadrootsBackgroundTransferState.self, forKey: .state), 107 progress: values.decode(RadrootsBackgroundTransferProgress.self, forKey: .progress), 108 failure: values.decodeIfPresent(RadrootsBackgroundTransferFailure.self, forKey: .failure), 109 response: values.decodeIfPresent(RadrootsBackgroundTransferResponse.self, forKey: .response), 110 downloadedArtifact: values.decodeIfPresent( 111 RadrootsBackgroundDownloadedArtifact.self, forKey: .downloadedArtifact 112 ), 113 possibleRemoteOrphan: values.decodeIfPresent(Bool.self, forKey: .possibleRemoteOrphan) 114 ?? false, 115 updatedAt: values.decode(Date.self, forKey: .updatedAt), 116 executionID: values.decodeIfPresent(UUID.self, forKey: .executionID), 117 uploadLease: values.decodeIfPresent(RadrootsStagedBlobReference.self, forKey: .uploadLease) 118 ) 119 } 120 121 public func encode(to encoder: any Encoder) throws { 122 var values = encoder.container(keyedBy: CodingKeys.self) 123 try values.encode(request, forKey: .request) 124 try values.encode(state, forKey: .state) 125 try values.encode(progress, forKey: .progress) 126 try values.encodeIfPresent(failure, forKey: .failure) 127 try values.encodeIfPresent(response, forKey: .response) 128 try values.encodeIfPresent(downloadedArtifact, forKey: .downloadedArtifact) 129 try values.encode(possibleRemoteOrphan, forKey: .possibleRemoteOrphan) 130 try values.encode(updatedAt, forKey: .updatedAt) 131 try values.encodeIfPresent(executionID, forKey: .executionID) 132 try values.encodeIfPresent(uploadLease, forKey: .uploadLease) 133 } 134 }