// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation public enum Upload { public typealias ProgressBlock = (Progress) -> Void public static let uploadQueue: OperationQueue = { let queue = OperationQueue() queue.name = "OWSUpload" queue.maxConcurrentOperationCount = CurrentAppContext().isNSE ? 2 : 8 return queue }() public enum Constants { public static let attachmentUploadProgressNotification = NSNotification.Name("AttachmentUploadProgressNotification") public static let uploadProgressKey = "UploadProgressKey" public static let uploadAttachmentIDKey = "UploadAttachmentIDKey" /// If within this window, we can reause existing attachment transit tier uploads for resending. public static let uploadReuseWindow: TimeInterval = 60 * 60 * 24 * 3 // 3 days } public enum FormSource { case remote case local(Upload.Form) } public struct Form: Codable { private enum CodingKeys: String, CodingKey { case headers = "headers" case signedUploadLocation = "signedUploadLocation" case cdnKey = "key" case cdnNumber = "cdn" } let headers: [String: String] let signedUploadLocation: String let cdnKey: String let cdnNumber: UInt32 } // MARK: - public enum FailureMode { public enum RetryMode { case immediately case afterDelay(TimeInterval) } // The overall upload has hit the max number of retries. case noMoreRetries // Attempt to resume the current upload from the last known good state. case resume(RetryMode) // Restart the upload by discarding any current upload progres and // fetching a new upload form. case restart(RetryMode) } public enum ResumeProgress { // There was an issue with the resume data, discard the current upload // form and restart the upload with a new form case restart // The endpoint reported a complete upload. case complete // Contains the number of bytes the upload endpoint has received. This // can be 0 bytes, which is effectively a new upload, but can use the // existing upload form. case uploaded(Int) } public enum Error: Swift.Error, IsRetryableProvider, LocalizedError { case invalidUploadURL case uploadFailure(recovery: FailureMode) case unsupportedEndpoint case unknown public var isRetryableProvider: Bool { switch self { case .invalidUploadURL, .uploadFailure, .unsupportedEndpoint, .unknown: return false } } public var errorDescription: String? { localizedDescription } public var localizedDescription: String { return OWSLocalizedString( "ERROR_MESSAGE_ATTACHMENT_UPLOAD_FAILED", comment: "Error message indicating that attachment upload(s) failed." ) } } public struct EncryptedBackupUploadMetadata: UploadMetadata { /// File URL of the data consisting of "iv + encrypted data + hmac" public let fileUrl: URL /// The digest of the encrypted file. The encrypted file consist of "iv + encrypted data + hmac" public let digest: Data /// The length of the encrypted data, consiting of "iv + encrypted data + hmac" public let encryptedDataLength: UInt32 /// The length of the unencrypted data public let plaintextDataLength: UInt32 } public struct LocalUploadMetadata: UploadMetadata { /// File URL of the data consisting of "iv + encrypted data + hmac" public let fileUrl: URL /// encryption key + hmac public let key: Data /// The digest of the encrypted file. The encrypted file consist of "iv + encrypted data + hmac" public let digest: Data /// The length of the encrypted data, consiting of "iv + encrypted data + hmac" public let encryptedDataLength: UInt32 /// The length of the unencrypted data public let plaintextDataLength: UInt32 } public struct Result { let cdnKey: String let cdnNumber: UInt32 let localUploadMetadata: Metadata // Timestamp the upload attempt began let beginTimestamp: UInt64 // Timestamp the upload attempt completed let finishTimestamp: UInt64 } public struct Attempt { let cdnKey: String let cdnNumber: UInt32 let localMetadata: UploadMetadata let beginTimestamp: UInt64 let endpoint: UploadEndpoint let uploadLocation: URL let logger: PrefixedLogger } } extension Upload.LocalUploadMetadata { static func validateAndBuild( fileUrl: URL, metadata: EncryptionMetadata ) throws -> Upload.LocalUploadMetadata { guard let lengthRaw = metadata.length, let plaintextLengthRaw = metadata.plaintextLength else { throw OWSAssertionError("Missing length.") } guard lengthRaw > 0, lengthRaw <= UInt32.max, plaintextLengthRaw > 0, plaintextLengthRaw <= UInt32.max else { throw OWSAssertionError("Invalid length.") } let length = UInt32(lengthRaw) let plaintextLength = UInt32(plaintextLengthRaw) guard plaintextLength <= OWSMediaUtils.kMaxFileSizeGeneric, length <= OWSMediaUtils.kMaxAttachmentUploadSizeBytes else { throw OWSAssertionError("Data is too large: \(length).") } guard let digest = metadata.digest else { throw OWSAssertionError("Digest missing for attachment.") } return Upload.LocalUploadMetadata( fileUrl: fileUrl, key: metadata.key, digest: digest, encryptedDataLength: length, plaintextDataLength: plaintextLength ) } }