Skip to content
Snippets Groups Projects
BackupService.swift 12.8 KiB
Newer Older
import UIKit
import Models
import Combine
import Defaults
import Keychain
Bruno Muniz's avatar
Bruno Muniz committed
import SFTPFeature
import iCloudFeature
import DropboxFeature
import NetworkMonitor
import GoogleDriveFeature
import DependencyInjection
Bruno Muniz's avatar
Bruno Muniz committed
import XXClient
import XXMessengerClient

public final class BackupService {
Bruno Muniz's avatar
Bruno Muniz committed
    @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
Bruno Muniz's avatar
Bruno Muniz committed
    @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?
Bruno Muniz's avatar
Bruno Muniz committed
    @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 {
Bruno Muniz's avatar
Bruno Muniz committed
    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)!)")
Bruno Muniz's avatar
Bruno Muniz committed
            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)
            }
Bruno Muniz's avatar
Bruno Muniz committed
        case .sftp:
Bruno Muniz's avatar
Bruno Muniz committed
            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)
        }

Bruno Muniz's avatar
Bruno Muniz committed
        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
                }

Bruno Muniz's avatar
Bruno Muniz committed
                settings.value.backups[.icloud] = BackupModel(
                    id: metadata.path,
                    date: metadata.modifiedDate,
                    size: metadata.size
                )
            }
        }

Bruno Muniz's avatar
Bruno Muniz committed
        if sftpService.isAuthorized() {
Bruno Muniz's avatar
Bruno Muniz committed
            sftpService.fetchMetadata { [weak settings] in
Bruno Muniz's avatar
Bruno Muniz committed
                guard let settings = settings else { return }

                guard let metadata = try? $0.get()?.backup else {
                    settings.value.backups[.sftp] = nil
                    return
Bruno Muniz's avatar
Bruno Muniz committed
                settings.value.backups[.sftp] = BackupModel(
Bruno Muniz's avatar
Bruno Muniz committed
                    id: metadata.id,
                    date: metadata.date,
                    size: metadata.size
                )
Bruno Muniz's avatar
Bruno Muniz committed
            }
        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
                }

Bruno Muniz's avatar
Bruno Muniz committed
                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 }

Bruno Muniz's avatar
Bruno Muniz committed
                    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)
                }
            }
Bruno Muniz's avatar
Bruno Muniz committed
        case .sftp:
            sftpService.uploadBackup(url: url) {
Bruno Muniz's avatar
Bruno Muniz committed
                switch $0 {
                case .success(let backup):
                    self.settings.value.backups[.sftp] = backup
                case .failure(let error):
                    print(error.localizedDescription)
                }