Use typed API for attachment upload form requests

This commit is contained in:
Pete Walters 2026-04-08 16:48:28 -05:00 committed by GitHub
parent 13e4f39d2f
commit 12c6075d52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 204 additions and 178 deletions

View File

@ -555,34 +555,6 @@ extension AppSetup.GlobalsContinuation {
)
let attachmentThumbnailService = AttachmentThumbnailServiceImpl(remoteConfigProvider: remoteConfigProvider)
let attachmentUploadManager = AttachmentUploadManagerImpl(
accountKeyStore: accountKeyStore,
attachmentEncrypter: Upload.Wrappers.AttachmentEncrypter(),
attachmentStore: attachmentStore,
attachmentUploadStore: attachmentUploadStore,
attachmentThumbnailService: attachmentThumbnailService,
backupRequestManager: backupRequestManager,
dateProvider: dateProvider,
db: db,
fileSystem: Upload.Wrappers.FileSystem(),
interactionStore: interactionStore,
networkManager: networkManager,
remoteConfigProvider: remoteConfigManager,
signalService: signalService,
sleepTimer: Upload.Wrappers.SleepTimer(),
storyStore: storyStore,
)
let attachmentBackfillManager = AttachmentBackfillManager(
attachmentStore: attachmentStore,
attachmentUploadManager: attachmentUploadManager,
db: db,
interactionStore: interactionStore,
notificationPresenter: notificationPresenter,
recipientDatabaseTable: recipientDatabaseTable,
syncMessageSender: messageSenderJobQueue,
threadStore: threadStore,
)
let backupAttachmentDownloadQueueStatusManager = BackupAttachmentDownloadQueueStatusManagerImpl(
appContext: appContext,
@ -678,77 +650,6 @@ extension AppSetup.GlobalsContinuation {
let backupAttachmentDownloadScheduler = BackupAttachmentDownloadSchedulerImpl(
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
)
let backupAttachmentCoordinator = testDependencies.backupAttachmentCoordinator ?? BackupAttachmentCoordinatorImpl(
appContext: appContext,
appReadiness: appReadiness,
backupSettingsStore: backupSettingsStore,
db: db,
downloadRunner: BackupAttachmentDownloadQueueRunnerImpl(
appContext: appContext,
attachmentStore: attachmentStore,
attachmentDownloadManager: attachmentDownloadManager,
attachmentUploadStore: attachmentUploadStore,
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
backupAttachmentUploadScheduler: backupAttachmentUploadScheduler,
backupMediaErrorNotificationPresenter: backupMediaErrorNotificationPresenter,
backupListMediaManager: backupListMediaManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
mediaBandwidthPreferenceStore: mediaBandwidthPreferenceStore,
progress: backupAttachmentDownloadProgress,
remoteConfigProvider: remoteConfigManager,
statusManager: backupAttachmentDownloadQueueStatusManager,
tsAccountManager: tsAccountManager,
),
listMediaManager: backupListMediaManager,
offloadingManager: AttachmentOffloadingManagerImpl(
attachmentStore: attachmentStore,
attachmentThumbnailService: attachmentThumbnailService,
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
backupAttachmentUploadEraStore: backupAttachmentUploadEraStore,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
listMediaManager: backupListMediaManager,
orphanedAttachmentCleaner: orphanedAttachmentCleaner,
orphanedAttachmentStore: orphanedAttachmentStore,
tsAccountManager: tsAccountManager,
),
orphanRunner: OrphanedBackupAttachmentQueueRunnerImpl(
accountKeyStore: accountKeyStore,
appReadiness: appReadiness,
attachmentStore: attachmentStore,
backupRequestManager: backupRequestManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
listMediaManager: backupListMediaManager,
orphanedBackupAttachmentStore: orphanedBackupAttachmentStore,
tsAccountManager: tsAccountManager,
),
orphanStore: orphanedBackupAttachmentStore,
tsAccountManager: tsAccountManager,
uploadRunner: BackupAttachmentUploadQueueRunnerImpl(
accountKeyStore: accountKeyStore,
attachmentStore: attachmentStore,
attachmentUploadManager: attachmentUploadManager,
backupAttachmentUploadScheduler: backupAttachmentUploadScheduler,
backupAttachmentUploadStore: backupAttachmentUploadStore,
backupAttachmentUploadEraStore: backupAttachmentUploadEraStore,
backupListMediaManager: backupListMediaManager,
backupMediaErrorNotificationPresenter: backupMediaErrorNotificationPresenter,
backupRequestManager: backupRequestManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
notificationPresenter: notificationPresenter,
orphanedBackupAttachmentStore: orphanedBackupAttachmentStore,
progress: backupAttachmentUploadProgress,
statusManager: backupAttachmentUploadQueueStatusManager,
tsAccountManager: tsAccountManager,
),
)
let attachmentManager = AttachmentManagerImpl(
attachmentDownloadManager: attachmentDownloadManager,
@ -1146,6 +1047,106 @@ extension AppSetup.GlobalsContinuation {
registrationStateChangeManager: registrationStateChangeManager,
)
let attachmentUploadManager = AttachmentUploadManagerImpl(
accountKeyStore: accountKeyStore,
attachmentEncrypter: Upload.Wrappers.AttachmentEncrypter(),
attachmentStore: attachmentStore,
attachmentUploadStore: attachmentUploadStore,
attachmentThumbnailService: attachmentThumbnailService,
backupRequestManager: backupRequestManager,
chatConnectionManager: chatConnectionManager,
dateProvider: dateProvider,
db: db,
fileSystem: Upload.Wrappers.FileSystem(),
interactionStore: interactionStore,
remoteConfigProvider: remoteConfigManager,
signalService: signalService,
sleepTimer: Upload.Wrappers.SleepTimer(),
storyStore: storyStore,
)
let backupAttachmentCoordinator = testDependencies.backupAttachmentCoordinator ?? BackupAttachmentCoordinatorImpl(
appContext: appContext,
appReadiness: appReadiness,
backupSettingsStore: backupSettingsStore,
db: db,
downloadRunner: BackupAttachmentDownloadQueueRunnerImpl(
appContext: appContext,
attachmentStore: attachmentStore,
attachmentDownloadManager: attachmentDownloadManager,
attachmentUploadStore: attachmentUploadStore,
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
backupAttachmentUploadScheduler: backupAttachmentUploadScheduler,
backupMediaErrorNotificationPresenter: backupMediaErrorNotificationPresenter,
backupListMediaManager: backupListMediaManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
mediaBandwidthPreferenceStore: mediaBandwidthPreferenceStore,
progress: backupAttachmentDownloadProgress,
remoteConfigProvider: remoteConfigManager,
statusManager: backupAttachmentDownloadQueueStatusManager,
tsAccountManager: tsAccountManager,
),
listMediaManager: backupListMediaManager,
offloadingManager: AttachmentOffloadingManagerImpl(
attachmentStore: attachmentStore,
attachmentThumbnailService: attachmentThumbnailService,
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
backupAttachmentUploadEraStore: backupAttachmentUploadEraStore,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
listMediaManager: backupListMediaManager,
orphanedAttachmentCleaner: orphanedAttachmentCleaner,
orphanedAttachmentStore: orphanedAttachmentStore,
tsAccountManager: tsAccountManager,
),
orphanRunner: OrphanedBackupAttachmentQueueRunnerImpl(
accountKeyStore: accountKeyStore,
appReadiness: appReadiness,
attachmentStore: attachmentStore,
backupRequestManager: backupRequestManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
listMediaManager: backupListMediaManager,
orphanedBackupAttachmentStore: orphanedBackupAttachmentStore,
tsAccountManager: tsAccountManager,
),
orphanStore: orphanedBackupAttachmentStore,
tsAccountManager: tsAccountManager,
uploadRunner: BackupAttachmentUploadQueueRunnerImpl(
accountKeyStore: accountKeyStore,
attachmentStore: attachmentStore,
attachmentUploadManager: attachmentUploadManager,
backupAttachmentUploadScheduler: backupAttachmentUploadScheduler,
backupAttachmentUploadStore: backupAttachmentUploadStore,
backupAttachmentUploadEraStore: backupAttachmentUploadEraStore,
backupListMediaManager: backupListMediaManager,
backupMediaErrorNotificationPresenter: backupMediaErrorNotificationPresenter,
backupRequestManager: backupRequestManager,
backupSettingsStore: backupSettingsStore,
dateProvider: dateProvider,
db: db,
notificationPresenter: notificationPresenter,
orphanedBackupAttachmentStore: orphanedBackupAttachmentStore,
progress: backupAttachmentUploadProgress,
statusManager: backupAttachmentUploadQueueStatusManager,
tsAccountManager: tsAccountManager,
),
)
let attachmentBackfillManager = AttachmentBackfillManager(
attachmentStore: attachmentStore,
attachmentUploadManager: attachmentUploadManager,
db: db,
interactionStore: interactionStore,
notificationPresenter: notificationPresenter,
recipientDatabaseTable: recipientDatabaseTable,
syncMessageSender: messageSenderJobQueue,
threadStore: threadStore,
)
let accountChecker = AccountChecker(
db: db,
networkManager: networkManager,

View File

@ -22,16 +22,6 @@ public enum OWSRequestFactory {
// MARK: - Other
static func allocAttachmentRequestV4(encryptedByteLength: UInt32) -> TSRequest {
var urlComps = URLComponents(string: "v4/attachments/form/upload")!
urlComps.queryItems = [URLQueryItem(name: "uploadLength", value: "\(encryptedByteLength)")]
return TSRequest(
url: urlComps.url!,
method: "GET",
parameters: [:],
)
}
static func currencyConversionRequest() -> TSRequest {
return TSRequest(url: URL(string: "v1/payments/conversions")!, method: "GET", parameters: [:])
}

View File

@ -47,6 +47,23 @@ public protocol ChatConnectionManager {
timeout: TimeInterval,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: UnauthServiceSelector
/// Access a libsignal "service" on the active authenticated connection.
///
/// Intended to be used with code completion; ``AuthServiceSelector``
/// has static members for each valid service. See the docs for that protocol
/// for under-the-hood information.
///
/// This will attempt to hold the connection open until the operation
/// completes, so make sure to do any complex processing of the result
/// *outside* the callback.
///
/// This method can be called from any thread.
func withAuthServiceImpl<Service, Output>(
_ service: Service,
timeout: TimeInterval,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: AuthServiceSelector
}
extension ChatConnectionManager {
@ -64,6 +81,14 @@ extension ChatConnectionManager {
) async throws -> Output where Service: UnauthServiceSelector {
return try await withUnauthServiceImpl(service, timeout: timeout, do: callback)
}
public func withAuthService<Service, Output>(
_ service: Service,
timeout: TimeInterval = .infinity,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: AuthServiceSelector {
return try await withAuthServiceImpl(service, timeout: timeout, do: callback)
}
}
public class ChatConnectionManagerImpl: ChatConnectionManager {
@ -178,6 +203,18 @@ public class ChatConnectionManagerImpl: ChatConnectionManager {
}
}
// This method can be called from any thread.
public func withAuthServiceImpl<Service, Output>(
_ service: Service,
timeout: TimeInterval,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: AuthServiceSelector {
try await connectionIdentified.withLibsignalConnection(timeout: timeout) { connection in
// This force-cast is guaranteed by AuthServiceSelector only being provided for valid service protocols.
try await callback(connection as! Service.Api)
}
}
// MARK: -
public var hasEmptiedInitialQueue: Bool {
@ -255,6 +292,14 @@ public class ChatConnectionManagerMock: ChatConnectionManager {
) async throws -> Output where Service: UnauthServiceSelector {
fatalError("must override for tests")
}
public func withAuthServiceImpl<Service, Output>(
_ service: Service,
timeout: TimeInterval,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: AuthServiceSelector {
fatalError("must override for tests")
}
}
#endif

View File

@ -395,31 +395,3 @@ public enum AttachmentUpload {
)
}
}
extension Upload {
struct FormRequest {
private let networkManager: NetworkManager
init(
networkManager: NetworkManager,
) {
self.networkManager = networkManager
}
func fetchForm(encryptedByteLength: UInt32) async throws -> Upload.Form {
let request = OWSRequestFactory.allocAttachmentRequestV4(encryptedByteLength: encryptedByteLength)
return try await fetchUploadForm(request: request)
}
private func fetchUploadForm<T: Decodable>(request: TSRequest) async throws -> T {
guard let data = try await performRequest(request).responseBodyData else {
throw OWSAssertionError("Invalid JSON")
}
return try JSONDecoder().decode(T.self, from: data)
}
private func performRequest(_ request: TSRequest) async throws -> HTTPResponse {
try await networkManager.asyncRequest(request)
}
}
}

View File

@ -64,11 +64,11 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
private let attachmentUploadStore: AttachmentUploadStore
private let attachmentThumbnailService: AttachmentThumbnailService
private let backupRequestManager: BackupRequestManager
private let chatConnectionManager: ChatConnectionManager
private let dateProvider: DateProvider
private let db: any DB
private let fileSystem: Upload.Shims.FileSystem
private let interactionStore: InteractionStore
private let networkManager: NetworkManager
private let remoteConfigProvider: any RemoteConfigProvider
private let signalService: OWSSignalServiceProtocol
private let sleepTimer: Upload.Shims.SleepTimer
@ -114,11 +114,11 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
attachmentUploadStore: AttachmentUploadStore,
attachmentThumbnailService: AttachmentThumbnailService,
backupRequestManager: BackupRequestManager,
chatConnectionManager: ChatConnectionManager,
dateProvider: @escaping DateProvider,
db: any DB,
fileSystem: Upload.Shims.FileSystem,
interactionStore: InteractionStore,
networkManager: NetworkManager,
remoteConfigProvider: any RemoteConfigProvider,
signalService: OWSSignalServiceProtocol,
sleepTimer: Upload.Shims.SleepTimer,
@ -130,11 +130,11 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
self.attachmentUploadStore = attachmentUploadStore
self.attachmentThumbnailService = attachmentThumbnailService
self.backupRequestManager = backupRequestManager
self.chatConnectionManager = chatConnectionManager
self.dateProvider = dateProvider
self.db = db
self.fileSystem = fileSystem
self.interactionStore = interactionStore
self.networkManager = networkManager
self.remoteConfigProvider = remoteConfigProvider
self.signalService = signalService
self.sleepTimer = sleepTimer
@ -188,9 +188,9 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
let sourceURL = dataSource.fileUrl
let metadata = try attachmentEncrypter.encryptAttachment(at: sourceURL, output: temporaryFile)
let localMetadata = try Upload.LocalUploadMetadata.validateAndBuild(fileUrl: temporaryFile, metadata: metadata)
let form = try await Upload.FormRequest(
networkManager: networkManager,
).fetchForm(encryptedByteLength: localMetadata.encryptedDataLength)
let form = try await chatConnectionManager.withAuthService(.attachments) {
try await $0.getUploadForm(uploadSize: UInt64(localMetadata.encryptedDataLength)).asUploadForm()
}
do {
// We don't show progress for transient uploads
@ -235,9 +235,9 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
throw OWSAssertionError("invalid link n sync attachment size")
}
let metadata = Upload.LinkNSyncUploadMetadata(fileUrl: sourceURL, encryptedDataLength: fileSize)
let form = try await Upload.FormRequest(
networkManager: networkManager,
).fetchForm(encryptedByteLength: metadata.encryptedDataLength)
let form = try await chatConnectionManager.withAuthService(.attachments) {
try await $0.getUploadForm(uploadSize: UInt64(metadata.encryptedDataLength)).asUploadForm()
}
do {
// We don't show progress for transient uploads
@ -723,12 +723,8 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
updateRecord = true
switch type {
case .transitTier:
do {
uploadForm = try await Upload.FormRequest(
networkManager: self.networkManager,
).fetchForm(encryptedByteLength: localMetadata.encryptedDataLength)
} catch {
throw error
uploadForm = try await chatConnectionManager.withAuthService(.attachments) {
try await $0.getUploadForm(uploadSize: UInt64(localMetadata.encryptedDataLength)).asUploadForm()
}
case .mediaTier(let auth, _):
uploadForm = try await self.backupRequestManager
@ -1198,3 +1194,18 @@ extension Upload {
}
}
}
extension Upload.Form {
public init(uploadForm: UploadForm) {
self.headers = HttpHeaders(httpHeaders: uploadForm.headers, overwriteOnConflict: true)
self.signedUploadLocation = uploadForm.signedUploadUrl.absoluteString
self.cdnKey = uploadForm.key
self.cdnNumber = uploadForm.cdn
}
}
extension UploadForm {
func asUploadForm() -> Upload.Form {
Upload.Form(uploadForm: self)
}
}

View File

@ -4,21 +4,22 @@
//
import Foundation
import LibSignalClient
@testable import SignalServiceKit
typealias PerformTSRequestBlock = (TSRequest) async throws -> HTTPResponse
typealias PerformUploadFormRequestBlock = () throws -> UploadForm
typealias PerformRequestBlock = (URLRequest) async throws -> HTTPResponse
typealias PerformUploadBlock = (URLRequest, Data, OWSURLSession.ProgressBlock) async throws -> HTTPResponse
enum MockRequestType {
case uploadForm(PerformTSRequestBlock)
case uploadForm(PerformUploadFormRequestBlock)
case uploadLocation(PerformRequestBlock)
case uploadProgress(PerformRequestBlock)
case uploadTask(PerformUploadBlock)
}
enum MockResultType {
case uploadForm(TSRequest)
case uploadForm
case uploadLocation(URLRequest)
case uploadProgress(URLRequest)
case uploadTask(URLRequest)
@ -73,7 +74,6 @@ class AttachmentUploadManagerMockHelper {
lazy var mockDateProvider = { return self.mockDate }
var mockDB = InMemoryDB()
var mockURLSession = AttachmentUploadManagerImpl.Mocks.URLSession()
var mockNetworkManager = AttachmentUploadManagerImpl.Mocks.NetworkManager(appReadiness: AppReadinessMock(), libsignalNet: nil)
var mockServiceManager = OWSSignalServiceMock()
var mockChatConnectionManager = AttachmentUploadManagerImpl.Mocks.ChatConnectionManager()
var mockFileSystem = AttachmentUploadManagerImpl.Mocks.FileSystem()
@ -140,13 +140,13 @@ class AttachmentUploadManagerMockHelper {
return self.mockURLSession
}
mockNetworkManager.performRequestBlock = { request in
mockChatConnectionManager.performRequestBlock = {
let item = self.authFormRequestBlock.removeFirst()
guard case let .uploadForm(authDataTaskBlock) = item else {
return .init(error: OWSAssertionError("Mock request missing"))
throw OWSAssertionError("Mock request missing")
}
self.capturedRequests.append(.uploadForm(request))
return Promise.wrapAsync { try await authDataTaskBlock(request) }
self.capturedRequests.append(.uploadForm)
return try authDataTaskBlock()
}
mockURLSession.performRequestBlock = { request in
@ -217,13 +217,13 @@ class AttachmentUploadManagerMockHelper {
cdnKey: UUID().uuidString,
cdnNumber: cdn.rawValue,
)
authFormRequestBlock.append(.uploadForm({ request in
authFormRequestBlock.append(.uploadForm({
self.activeUploadRequestMocks = self.authToUploadRequestMockMap[authString] ?? .init()
return HTTPResponse(
requestUrl: request.url,
status: statusCode,
headers: HttpHeaders(),
bodyData: try! JSONEncoder().encode(form),
return UploadForm(
cdn: form.cdnNumber,
key: form.cdnKey,
headers: headers.headers,
signedUploadUrl: URL(string: location)!,
)
}))
return .init(

View File

@ -9,7 +9,6 @@ import LibSignalClient
extension AttachmentUploadManagerImpl {
enum Mocks {
typealias NetworkManager = _AttachmentUploadManager_NetworkManagerMock
typealias URLSession = _AttachmentUploadManager_OWSURLSessionMock
typealias ChatConnectionManager = _AttachmentUploadManager_ChatConnectionManagerMock
@ -60,15 +59,6 @@ class _Upload_SleepTimerMock: Upload.Shims.SleepTimer {
}
}
class _AttachmentUploadManager_NetworkManagerMock: NetworkManager {
var performRequestBlock: ((TSRequest) -> Promise<HTTPResponse>)?
override func asyncRequestImpl(_ request: TSRequest, retryPolicy: RetryPolicy) async throws -> HTTPResponse {
return try await performRequestBlock!(request).awaitable()
}
}
public class _AttachmentUploadManager_OWSURLSessionMock: BaseOWSURLSessionMock {
public var performUploadDataBlock: ((URLRequest, Data, OWSURLSession.ProgressBlock) async throws -> HTTPResponse)?
@ -87,7 +77,24 @@ public class _AttachmentUploadManager_OWSURLSessionMock: BaseOWSURLSessionMock {
}
}
class _AttachmentUploadManager_ChatConnectionManagerMock: ChatConnectionManagerMock {}
struct MockAuthMessageService: AuthMessagesService {
var performRequestBlock: (@Sendable () throws -> UploadForm)
func getUploadForm(uploadSize: UInt64) async throws -> UploadForm {
try performRequestBlock()
}
}
class _AttachmentUploadManager_ChatConnectionManagerMock: ChatConnectionManagerMock {
var performRequestBlock: (@Sendable () throws -> UploadForm)?
override func withAuthServiceImpl<Service, Output>(
_ service: Service,
timeout: TimeInterval,
do callback: @escaping (Service.Api) async throws -> Output,
) async throws -> Output where Service: AuthServiceSelector {
let service = MockAuthMessageService(performRequestBlock: performRequestBlock!)
return try await callback(service as! Service.Api)
}
}
class _AttachmentUploadManager_BackupRequestManagerMock: BackupRequestManager {
func fetchBackupServiceAuthForRegistration(

View File

@ -19,11 +19,11 @@ struct AttachmentUploadManagerTests {
attachmentUploadStore: helper.mockAttachmentUploadStore,
attachmentThumbnailService: helper.mockAttachmentThumbnailService,
backupRequestManager: helper.mockBackupRequestManager,
chatConnectionManager: helper.mockChatConnectionManager,
dateProvider: helper.mockDateProvider,
db: helper.mockDB,
fileSystem: helper.mockFileSystem,
interactionStore: helper.mockInteractionStore,
networkManager: helper.mockNetworkManager,
remoteConfigProvider: helper.mockRemoteConfigProvider,
signalService: helper.mockServiceManager,
sleepTimer: helper.mockSleepTimer,