import UIKit import Models import Combine import Defaults import Keychain import SFTPFeature import iCloudFeature import DropboxFeature import NetworkMonitor import GoogleDriveFeature import DependencyInjection import XXClient import XXMessengerClient public final class BackupService { @Dependency var messenger: Messenger @Dependency var sftpService: SFTPService @Dependency var icloudService: iCloudInterface @Dependency var dropboxService: DropboxInterface @Dependency var networkManager: NetworkMonitoring @Dependency var keychainHandler: KeychainHandling @Dependency var driveService: GoogleDriveInterface @KeyObject(.email, defaultValue: nil) var email: String? @KeyObject(.phone, defaultValue: nil) var phone: String? @KeyObject(.username, defaultValue: nil) var username: String? var manager: XXClient.Backup? @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data public var settingsPublisher: AnyPublisher<BackupSettings, Never> { settings.handleEvents(receiveSubscription: { [weak self] _ in guard let self = self else { return } let lastRefreshDate = self.settingsLastRefreshedDate ?? Date.distantPast if Date().timeIntervalSince(lastRefreshDate) < 10 { return } self.settingsLastRefreshedDate = Date() self.refreshConnections() self.refreshBackups() }).eraseToAnyPublisher() } private var connType: ConnectionType = .wifi private var settingsLastRefreshedDate: Date? private var cancellables = Set<AnyCancellable>() private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) public init() { settings .dropFirst() .removeDuplicates() .sink { [unowned self] in storedSettings = $0.toData() } .store(in: &cancellables) networkManager.connType .receive(on: DispatchQueue.main) .sink { [unowned self] in connType = $0 } .store(in: &cancellables) } } extension BackupService { public func initializeBackup(passphrase: String) { manager = try! InitializeBackup.live( e2eId: messenger.e2e.get()!.getId(), udId: messenger.ud.get()!.getId(), password: passphrase, callback: .init(handle: { [weak self] backupData in self?.updateBackup(data: backupData) }) ) didUpdateFacts() } public func didUpdateFacts() { if let manager = manager { let currentFacts = try! JSONEncoder().encode( BackupParams( username: username!, email: email, phone: phone ) ) print(">>> Will addJSON: \(String(data: currentFacts, encoding: .utf8)!)") manager.addJSON(String(data: currentFacts, encoding: .utf8)!) } performBackup() } public func performBackupIfAutomaticIsEnabled() { guard settings.value.automaticBackups == true else { return } performBackup() } public func performBackup() { print(">>> Did call performBackup()") guard let directoryUrl = try? FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) else { fatalError("Couldn't generate the URL to persist the backup") } let fileUrl = directoryUrl .appendingPathComponent("backup") .appendingPathExtension("xxm") guard let data = try? Data(contentsOf: fileUrl) else { print(">>> Tried to backup arbitrarily but there was nothing to be backed up. Aborting...") return } performBackup(data: data) } public func updateBackup(data: Data) { print(">>> Did call updateBackup(data)") guard let directoryUrl = try? FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) else { fatalError("Couldn't generate the URL to persist the backup") } let fileUrl = directoryUrl .appendingPathComponent("backup") .appendingPathExtension("xxm") do { try data.write(to: fileUrl) } catch { fatalError("Couldn't write backup to fileurl") } let isWifiOnly = settings.value.wifiOnlyBackup let isAutomaticEnabled = settings.value.automaticBackups let hasEnabledService = settings.value.enabledService != nil if isWifiOnly { guard connType == .wifi else { return } } else { guard connType != .unknown else { return } } if isAutomaticEnabled && hasEnabledService { performBackup() } } public func setBackupOnlyOnWifi(_ enabled: Bool) { settings.value.wifiOnlyBackup = enabled } public func setBackupAutomatically(_ enabled: Bool) { settings.value.automaticBackups = enabled guard enabled else { return } performBackup() } public func toggle(service: CloudService, enabling: Bool) { settings.value.enabledService = enabling ? service : nil } public func authorize(service: CloudService, presenting screen: UIViewController) { switch service { case .drive: driveService.authorize(presenting: screen) { [weak self] _ in guard let self = self else { return } self.refreshConnections() self.refreshBackups() } case .icloud: if !icloudService.isAuthorized() { icloudService.openSettings() } else { refreshConnections() refreshBackups() } case .dropbox: if !dropboxService.isAuthorized() { dropboxService.authorize(presenting: screen) .sink { [weak self] _ in guard let self = self else { return } self.refreshConnections() self.refreshBackups() }.store(in: &cancellables) } case .sftp: if !sftpService.isAuthorized() { sftpService.authorizeFlow((screen, { [weak self] in guard let self = self else { return } screen.navigationController?.popViewController(animated: true) self.refreshConnections() self.refreshBackups() })) } } } } extension BackupService { private func refreshConnections() { if icloudService.isAuthorized() && !settings.value.connectedServices.contains(.icloud) { settings.value.connectedServices.insert(.icloud) } else if !icloudService.isAuthorized() && settings.value.connectedServices.contains(.icloud) { settings.value.connectedServices.remove(.icloud) } if dropboxService.isAuthorized() && !settings.value.connectedServices.contains(.dropbox) { settings.value.connectedServices.insert(.dropbox) } else if !dropboxService.isAuthorized() && settings.value.connectedServices.contains(.dropbox) { settings.value.connectedServices.remove(.dropbox) } if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) { settings.value.connectedServices.insert(.sftp) } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) { settings.value.connectedServices.remove(.sftp) } driveService.isAuthorized { [weak settings] isAuthorized in guard let settings = settings else { return } if isAuthorized && !settings.value.connectedServices.contains(.drive) { settings.value.connectedServices.insert(.drive) } else if !isAuthorized && settings.value.connectedServices.contains(.drive) { settings.value.connectedServices.remove(.drive) } } } private func refreshBackups() { if icloudService.isAuthorized() { icloudService.downloadMetadata { [weak settings] in guard let settings = settings else { return } guard let metadata = try? $0.get() else { settings.value.backups[.icloud] = nil return } settings.value.backups[.icloud] = BackupModel( id: metadata.path, date: metadata.modifiedDate, size: metadata.size ) } } if sftpService.isAuthorized() { sftpService.fetchMetadata { [weak settings] in guard let settings = settings else { return } guard let metadata = try? $0.get()?.backup else { settings.value.backups[.sftp] = nil return } settings.value.backups[.sftp] = BackupModel( id: metadata.id, date: metadata.date, size: metadata.size ) } } if dropboxService.isAuthorized() { dropboxService.downloadMetadata { [weak settings] in guard let settings = settings else { return } guard let metadata = try? $0.get() else { settings.value.backups[.dropbox] = nil return } settings.value.backups[.dropbox] = BackupModel( id: metadata.path, date: metadata.modifiedDate, size: metadata.size ) } } driveService.isAuthorized { [weak settings] isAuthorized in guard let settings = settings else { return } if isAuthorized { self.driveService.downloadMetadata { guard let metadata = try? $0.get() else { return } settings.value.backups[.drive] = BackupModel( id: metadata.identifier, date: metadata.modifiedDate, size: metadata.size ) } } else { settings.value.backups[.drive] = nil } } } private func performBackup(data: Data) { print(">>> Did call performBackup(data)") guard let enabledService = settings.value.enabledService else { fatalError("Trying to backup but nothing is enabled") } let url = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent(UUID().uuidString) do { try data.write(to: url, options: .atomic) } catch { print(">>> Couldn't write to temp: \(error.localizedDescription)") return } switch enabledService { case .drive: driveService.uploadBackup(url) { switch $0 { case .success(let metadata): self.settings.value.backups[.drive] = .init( id: metadata.identifier, date: metadata.modifiedDate, size: metadata.size ) case .failure(let error): print(error.localizedDescription) } } case .icloud: icloudService.uploadBackup(url) { switch $0 { case .success(let metadata): self.settings.value.backups[.icloud] = .init( id: metadata.path, date: metadata.modifiedDate, size: metadata.size ) case .failure(let error): print(error.localizedDescription) } } case .dropbox: dropboxService.uploadBackup(url) { switch $0 { case .success(let metadata): self.settings.value.backups[.dropbox] = .init( id: metadata.path, date: metadata.modifiedDate, size: metadata.size ) case .failure(let error): print(error.localizedDescription) } } case .sftp: sftpService.uploadBackup(url: url) { switch $0 { case .success(let backup): self.settings.value.backups[.sftp] = backup case .failure(let error): print(error.localizedDescription) } } } } }