Signal-iOS/SignalServiceKit/Backups/Settings/BackupRefreshManager.swift
2025-08-27 15:56:05 -07:00

122 lines
4.0 KiB
Swift

//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
// MARK: -
public final class BackupRefreshManager {
private let accountKeyStore: AccountKeyStore
private let api: NetworkAPI
private let backupRequestManager: BackupRequestManager
private let backupSettingsStore: BackupSettingsStore
private let db: DB
private let dateProvider: DateProvider
// Backups may expire after 30 days. Do a refresh every few days
// to account for if the app isn't opened often.
private let backupRefreshTimeInterval: TimeInterval = 3 * .day
init(
accountKeyStore: AccountKeyStore,
backupRequestManager: BackupRequestManager,
backupSettingsStore: BackupSettingsStore,
db: DB,
networkManager: NetworkManagerProtocol,
dateProvider: @escaping DateProvider
) {
self.accountKeyStore = accountKeyStore
self.api = NetworkAPI(networkManager: networkManager)
self.backupRequestManager = backupRequestManager
self.backupSettingsStore = backupSettingsStore
self.db = db
self.dateProvider = dateProvider
}
private func needsBackupRefresh() -> Bool {
db.read { tx in
let maxDaysAgo = dateProvider().addingTimeInterval(-1 * backupRefreshTimeInterval)
let backupPlan = backupSettingsStore.backupPlan(tx: tx)
let lastBackupRefreshDate = backupSettingsStore.lastBackupRefreshDate(tx: tx) ?? Date.distantPast
return backupPlan != .disabled && lastBackupRefreshDate < maxDaysAgo
}
}
private func rootBackupKeys(localIdentifiers: LocalIdentifiers) async throws -> (MessageRootBackupKey, MediaRootBackupKey) {
try await db.awaitableWrite { tx in
guard let messageRootBackupKey = try? accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx) else {
throw OWSAssertionError("Missing message root backup key! Do we not have an AEP?")
}
// If we don't yet have an MRBK, this is an appropriate point to
// generate one.
let mediaRootBackupKey = accountKeyStore.getOrGenerateMediaRootBackupKey(tx: tx)
return (messageRootBackupKey, mediaRootBackupKey)
}
}
public func refreshBackupIfNeeded(
localIdentifiers: LocalIdentifiers,
auth: ChatServiceAuth
) async throws {
guard FeatureFlags.Backups.supported else {
return
}
guard needsBackupRefresh() else {
return
}
let (messageBackupKey, mediaBackupKey) = try await rootBackupKeys(localIdentifiers: localIdentifiers)
let messageBackupAuth = try await backupRequestManager.fetchBackupServiceAuth(
for: messageBackupKey,
localAci: localIdentifiers.aci,
auth: auth
)
try await api.refreshBackup(auth: messageBackupAuth)
let mediaBackupAuth = try await backupRequestManager.fetchBackupServiceAuth(
for: mediaBackupKey,
localAci: localIdentifiers.aci,
auth: auth
)
try await api.refreshBackup(auth: mediaBackupAuth)
await db.awaitableWrite { tx in
backupSettingsStore.setLastBackupRefreshDate(dateProvider(), tx: tx)
}
}
// MARK: -
private struct NetworkAPI {
private let networkManager: NetworkManagerProtocol
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
func refreshBackup(auth: BackupServiceAuth) async throws {
_ = try await networkManager.asyncRequest(.refreshBackup(auth: auth))
}
}
}
// MARK: -
private extension TSRequest {
static func refreshBackup(auth: BackupServiceAuth) -> TSRequest {
var request = TSRequest(
url: URL(string: "v1/archives")!,
method: "POST",
parameters: [:]
)
request.auth = .backup(auth)
return request
}
}