171 lines
5.9 KiB
Swift
171 lines
5.9 KiB
Swift
//
|
|
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
|
|
/// Reponsible for "disabling Backups": making the relevant API calls and
|
|
/// managing state.
|
|
class BackupDisablingManager {
|
|
struct NotRegisteredError: Error {}
|
|
|
|
private enum StoreKeys {
|
|
static let attemptingDisableRemotely = "attemptingDisableRemotely"
|
|
static let remoteDisablingFailed = "remoteDisablingFailed"
|
|
}
|
|
|
|
private let backupIdManager: BackupIdManager
|
|
private let backupSettingsStore: BackupSettingsStore
|
|
private let db: DB
|
|
private let kvStore: KeyValueStore
|
|
private let logger: PrefixedLogger
|
|
private let taskQueue: ConcurrentTaskQueue
|
|
private let tsAccountManager: TSAccountManager
|
|
|
|
/// Tracks async work to disable remotely, if necessary. Calls to `.run()`
|
|
/// will (almost-)insta-complete if disabling remotely is not necessary.
|
|
private var disableRemotelyIfNecessaryTask: DebouncedTask<Void>!
|
|
|
|
init(
|
|
backupIdManager: BackupIdManager,
|
|
backupSettingsStore: BackupSettingsStore,
|
|
db: DB,
|
|
tsAccountManager: TSAccountManager,
|
|
) {
|
|
self.backupIdManager = backupIdManager
|
|
self.backupSettingsStore = backupSettingsStore
|
|
self.db = db
|
|
self.kvStore = KeyValueStore(collection: "BackupDisablingManager")
|
|
self.logger = PrefixedLogger(prefix: "[Backups]")
|
|
self.taskQueue = ConcurrentTaskQueue(concurrentLimit: 1)
|
|
self.tsAccountManager = tsAccountManager
|
|
|
|
self.disableRemotelyIfNecessaryTask = DebouncedTask { [weak self] in
|
|
guard let self else { return }
|
|
|
|
do {
|
|
if try await _disableRemotelyIfNecessaryWithIndefiniteNetworkRetries() {
|
|
logger.info("Disabled Backups remotely.")
|
|
}
|
|
} catch {
|
|
await db.awaitableWrite { tx in
|
|
self.kvStore.setBool(true, key: StoreKeys.remoteDisablingFailed, transaction: tx)
|
|
}
|
|
|
|
logger.error("Failed to disable Backups remotely! \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
enum DisableRemotelyState {
|
|
case inProgress(Task<Void, Error>)
|
|
case previouslyFailed
|
|
}
|
|
|
|
/// Disable Backups for the current user. Backups are immediately disabled
|
|
/// locally, with disabling remotely kicked off asynchronously.
|
|
///
|
|
/// Callers should call `currentDisableRemotelyState` after calling this
|
|
/// method to track the progress of disabling remotely.
|
|
///
|
|
/// - Throws `NotRegisteredError` before disabling if the user is not registered.
|
|
func disableBackups(tx: DBWriteTransaction) throws(NotRegisteredError) {
|
|
logger.info("Disabling Backups...")
|
|
|
|
guard tsAccountManager.localIdentifiers(tx: tx) != nil else {
|
|
logger.info("Can't disable Backups, not registered!")
|
|
throw NotRegisteredError()
|
|
}
|
|
|
|
backupSettingsStore.setBackupPlan(.disabled, tx: tx)
|
|
kvStore.setBool(true, key: StoreKeys.attemptingDisableRemotely, transaction: tx)
|
|
|
|
tx.addSyncCompletion { [self] in
|
|
logger.info("Disabled Backups locally. Disabling remotely...")
|
|
_ = disableRemotelyIfNecessaryTask.run()
|
|
}
|
|
}
|
|
|
|
/// Attempts to remotely disable Backups, if necessary. For example, a
|
|
/// previous launch may have attempted but failed to remotely disable
|
|
/// Backups.
|
|
func disableRemotelyIfNecessary() async throws {
|
|
// If we don't need to disable remotely, this will insta-complete.
|
|
try await disableRemotelyIfNecessaryTask.run().value
|
|
}
|
|
|
|
func currentDisableRemotelyState(tx: DBReadTransaction) -> DisableRemotelyState? {
|
|
if let task = disableRemotelyIfNecessaryTask.isCurrentlyRunning() {
|
|
return .inProgress(task)
|
|
}
|
|
|
|
switch backupSettingsStore.backupPlan(tx: tx) {
|
|
case .disabled:
|
|
if kvStore.hasValue(StoreKeys.remoteDisablingFailed, transaction: tx) {
|
|
return .previouslyFailed
|
|
}
|
|
case .free, .paid, .paidExpiringSoon:
|
|
break
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func _disableRemotelyIfNecessaryWithIndefiniteNetworkRetries() async throws -> Bool {
|
|
return try await Retry.performWithBackoff(
|
|
maxAttempts: .max,
|
|
maxAverageBackoff: 2 * .minute,
|
|
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
|
|
) {
|
|
return try await taskQueue.run {
|
|
try await _disableRemotelyIfNecessary()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// - Returns
|
|
/// A boolean indicating if disabling remotely was necessary.
|
|
private func _disableRemotelyIfNecessary() async throws -> Bool {
|
|
let localIdentifiers: LocalIdentifiers? = try db.read { tx in
|
|
let disabledLocally = switch backupSettingsStore.backupPlan(tx: tx) {
|
|
case .disabled: true
|
|
case .free, .paid, .paidExpiringSoon: false
|
|
}
|
|
let attemptingDisableRemotely = kvStore.hasValue(StoreKeys.attemptingDisableRemotely, transaction: tx)
|
|
|
|
guard disabledLocally && attemptingDisableRemotely else {
|
|
return nil
|
|
}
|
|
|
|
if let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) {
|
|
return localIdentifiers
|
|
} else {
|
|
throw NotRegisteredError()
|
|
}
|
|
}
|
|
|
|
guard let localIdentifiers else {
|
|
// We no longer need to disable remotely. Bail!
|
|
return false
|
|
}
|
|
|
|
try await backupIdManager.deleteBackupId(
|
|
localIdentifiers: localIdentifiers,
|
|
auth: .implicit()
|
|
)
|
|
|
|
await db.awaitableWrite { tx in
|
|
kvStore.removeValue(forKey: StoreKeys.remoteDisablingFailed, transaction: tx)
|
|
kvStore.removeValue(forKey: StoreKeys.attemptingDisableRemotely, transaction: tx)
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|