Skip to content
Snippets Groups Projects
BackupService.swift 11.6 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

public final class BackupService {
Bruno Muniz's avatar
Bruno Muniz committed
    @Dependency private var sftpService: SFTPService
    @Dependency private var icloudService: iCloudInterface
    @Dependency private var dropboxService: DropboxInterface
    @Dependency private var networkManager: NetworkMonitoring
    @Dependency private var keychainHandler: KeychainHandling
Bruno Muniz's avatar
Bruno Muniz committed
    @Dependency private var driveService: GoogleDriveInterface

    @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data

Ahmed Shehata's avatar
Ahmed Shehata committed
    public var passphrase: String?

    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 performBackupIfAutomaticIsEnabled() {
        guard settings.value.automaticBackups == true else { return }
        performBackup()
    }

    public func 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) {
        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
                }

                settings.value.backups[.icloud] = Backup(
                    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
                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] = Backup(
                    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] = Backup(
                    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] = Backup(
                        id: metadata.identifier,
                        date: metadata.modifiedDate,
                        size: metadata.size
                    )
                }
            } else {
                settings.value.backups[.drive] = nil
            }
        }
    }

    private func performBackup(data: 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)
        } 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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
        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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
        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)
                }

                // try? FileManager.default.removeItem(at: url)
            }
Bruno Muniz's avatar
Bruno Muniz committed
        case .sftp:
Bruno Muniz's avatar
Bruno Muniz committed
                try sftpService.uploadBackup(url)
            } catch {
                print(error.localizedDescription)
            }