TM-SGNL-iOS/SignalServiceKit/Upload/Upload.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

242 lines
7.7 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public enum Upload {
public static let uploadQueue = ConcurrentTaskQueue(concurrentLimit: CurrentAppContext().isNSE ? 2 : 8)
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 static let uploadFormReuseWindow: TimeInterval = 60 * 60 * 24 * 6 // 6 days
public static let maxUploadAttempts = 5
}
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 unexpectedResponseStatusCode(Int)
case unknown
public var isRetryableProvider: Bool {
switch self {
case .invalidUploadURL, .uploadFailure, .unsupportedEndpoint, .unexpectedResponseStatusCode, .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: AttachmentUploadMetadata, Codable {
/// 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 var isReusedTransitTierUpload: Bool { false }
}
public struct LinkNSyncUploadMetadata: UploadMetadata {
/// File URL of the link'n'sync transient backup.
public let fileUrl: URL
/// The length of the file.
public let encryptedDataLength: UInt32
}
public struct ReusedUploadMetadata: AttachmentUploadMetadata {
public let cdnKey: String
public let cdnNumber: UInt32
/// 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 unencrypted data
public let plaintextDataLength: UInt32
/// The length of the encrypted data, consiting of "iv + encrypted data + hmac"
public let encryptedDataLength: UInt32
public var isReusedTransitTierUpload: Bool { false }
}
public struct Result<Metadata: UploadMetadata> {
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 AttachmentResult {
let cdnKey: String
let cdnNumber: UInt32
let localUploadMetadata: AttachmentUploadMetadata
// Timestamp the upload attempt began
let beginTimestamp: UInt64
// Timestamp the upload attempt completed
let finishTimestamp: UInt64
}
public struct Attempt<Metadata: UploadMetadata> {
let cdnKey: String
let cdnNumber: UInt32
/// File URL of the data consisting of "iv + encrypted data + hmac"
let fileUrl: URL
/// The length of the encrypted data, consiting of "iv + encrypted data + hmac"
let encryptedDataLength: UInt32
let localMetadata: Metadata
let beginTimestamp: UInt64
let endpoint: UploadEndpoint
let uploadLocation: URL
let isResumedUpload: Bool
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
)
}
}