Skip to content
Snippets Groups Projects
BackupService.swift 11 KiB
Newer Older
import UIKit
import Models
import Combine
import Defaults
import Keychain
import NetworkMonitor
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

  @KeyObject(.username, defaultValue: nil) var username: String?
Bruno Muniz's avatar
Bruno Muniz committed
  @KeyObject(.backupSettings, defaultValue: nil) var storedSettings: Data?
Bruno Muniz's avatar
Bruno Muniz committed

  public var settingsPublisher: AnyPublisher<BackupSettings, Never> {
    settings.handleEvents(receiveSubscription: { [weak self] _ in
      guard let self = self else { return }
      self.refreshConnections()
      self.refreshBackups()
    }).eraseToAnyPublisher()
  }

  private var connType: ConnectionType = .wifi
  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 stopBackups() {
    print(">>> [AccountBackup] Requested to stop backup mechanism")
Bruno Muniz's avatar
Bruno Muniz committed
    if messenger.isBackupRunning() == true {
      print(">>> [AccountBackup] messenger.isBackupRunning() == true")
      try! messenger.stopBackup()

      print(">>> [AccountBackup] Stopped backup mechanism")
Bruno Muniz's avatar
Bruno Muniz committed
    }
Bruno Muniz's avatar
Bruno Muniz committed
  }
Bruno Muniz's avatar
Bruno Muniz committed
  public func initializeBackup(passphrase: String) {
    try! messenger.startBackup(
      password: passphrase,
      params: .init(username: username!)
    )
Bruno Muniz's avatar
Bruno Muniz committed
    print(">>> [AccountBackup] Initialized backup mechanism")
  }
Bruno Muniz's avatar
Bruno Muniz committed
  public func performBackupIfAutomaticIsEnabled() {
    print(">>> [AccountBackup] Requested backup if automatic is enabled")
Bruno Muniz's avatar
Bruno Muniz committed
    guard settings.value.automaticBackups == true else { return }
    performBackup()
  }
Bruno Muniz's avatar
Bruno Muniz committed
  public func performBackup() {
    print(">>> [AccountBackup] Requested backup without explicitly passing data")
Bruno Muniz's avatar
Bruno Muniz committed
    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") }
Bruno Muniz's avatar
Bruno Muniz committed
    let fileUrl = directoryUrl
      .appendingPathComponent("backup")
      .appendingPathExtension("xxm")
Bruno Muniz's avatar
Bruno Muniz committed
    guard let data = try? Data(contentsOf: fileUrl) else {
      print(">>> [AccountBackup] Tried to backup arbitrarily but there was nothing to be backed up. Aborting...")
      return
Bruno Muniz's avatar
Bruno Muniz committed
    performBackup(data: data)
  }
Bruno Muniz's avatar
Bruno Muniz committed
  public func updateBackup(data: Data) {
    print(">>> [AccountBackup] Requested to update backup passing data")
Bruno Muniz's avatar
Bruno Muniz committed
    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") }
Bruno Muniz's avatar
Bruno Muniz committed
    let fileUrl = directoryUrl
      .appendingPathComponent("backup")
      .appendingPathExtension("xxm")
Bruno Muniz's avatar
Bruno Muniz committed
    do {
      try data.write(to: fileUrl)
    } catch {
      fatalError("Couldn't write backup to fileurl")
    }
Bruno Muniz's avatar
Bruno Muniz committed
    let isWifiOnly = settings.value.wifiOnlyBackup
    let isAutomaticEnabled = settings.value.automaticBackups
    let hasEnabledService = settings.value.enabledService != nil
Bruno Muniz's avatar
Bruno Muniz committed
    if isWifiOnly {
      guard connType == .wifi else { return }
    } else {
      guard connType != .unknown else { return }
Bruno Muniz's avatar
Bruno Muniz committed
    if isAutomaticEnabled && hasEnabledService {
      performBackup()
    }
Bruno Muniz's avatar
Bruno Muniz committed

    refreshBackups()
Bruno Muniz's avatar
Bruno Muniz committed
  }

  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()
        }))
      }
Bruno Muniz's avatar
Bruno Muniz committed
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)
    }
Bruno Muniz's avatar
Bruno Muniz committed
    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)
Bruno Muniz's avatar
Bruno Muniz committed
    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)
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }
Bruno Muniz's avatar
Bruno Muniz committed
  private func refreshBackups() {
Bruno Muniz's avatar
Bruno Muniz committed
    print(">>> Refreshing backups...")

Bruno Muniz's avatar
Bruno Muniz committed
    if icloudService.isAuthorized() {
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Refreshing icloud backup...")

Bruno Muniz's avatar
Bruno Muniz committed
      icloudService.downloadMetadata { [weak settings] in
        guard let settings = settings else { return }
Bruno Muniz's avatar
Bruno Muniz committed
        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

Bruno Muniz's avatar
Bruno Muniz committed
    if sftpService.isAuthorized() {
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Refreshing sftp backup...")

Bruno Muniz's avatar
Bruno Muniz committed
      sftpService.fetchMetadata { [weak settings] in
        guard let settings = settings else { return }
Bruno Muniz's avatar
Bruno Muniz committed
        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(
          id: metadata.id,
          date: metadata.date,
          size: metadata.size
        )
      }
Bruno Muniz's avatar
Bruno Muniz committed
    if dropboxService.isAuthorized() {
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Refreshing dropbox backup...")

Bruno Muniz's avatar
Bruno Muniz committed
      dropboxService.downloadMetadata { [weak settings] in
        guard let settings = settings else { return }
Bruno Muniz's avatar
Bruno Muniz committed
        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
        )
      }
    }
Bruno Muniz's avatar
Bruno Muniz committed
    driveService.isAuthorized { [weak settings] isAuthorized  in
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Refreshing drive backup...")
Bruno Muniz's avatar
Bruno Muniz committed
      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
          )
Bruno Muniz's avatar
Bruno Muniz committed
      } else {
        settings.value.backups[.drive] = nil
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }
Bruno Muniz's avatar
Bruno Muniz committed
  private func performBackup(data: Data) {
    print(">>> Did call performBackup(data)")
Bruno Muniz's avatar
Bruno Muniz committed
    guard let enabledService = settings.value.enabledService else {
      fatalError("Trying to backup but nothing is enabled")
    }
Bruno Muniz's avatar
Bruno Muniz committed
    let url = URL(fileURLWithPath: NSTemporaryDirectory())
      .appendingPathComponent(UUID().uuidString)
Bruno Muniz's avatar
Bruno Muniz committed
    do {
      try data.write(to: url, options: .atomic)
    } catch {
      print(">>> Couldn't write to temp: \(error.localizedDescription)")
      return
    }
Bruno Muniz's avatar
Bruno Muniz committed
    switch enabledService {
    case .drive:
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Performing upload on drive")
Bruno Muniz's avatar
Bruno Muniz committed
      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:
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Performing upload on iCloud")
Bruno Muniz's avatar
Bruno Muniz committed
      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:
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Performing upload on dropbox")
Bruno Muniz's avatar
Bruno Muniz committed
      dropboxService.uploadBackup(url) {
        switch $0 {
        case .success(let metadata):
Bruno Muniz's avatar
Bruno Muniz committed
          print(">>> Performed upload on dropbox: \(metadata)")

Bruno Muniz's avatar
Bruno Muniz committed
          self.settings.value.backups[.dropbox] = .init(
            id: metadata.path,
            date: metadata.modifiedDate,
            size: metadata.size
          )
Bruno Muniz's avatar
Bruno Muniz committed

          self.refreshBackups()
Bruno Muniz's avatar
Bruno Muniz committed
        case .failure(let error):
          print(error.localizedDescription)
        }
      }
    case .sftp:
Bruno Muniz's avatar
Bruno Muniz committed
      print(">>> Performing upload on sftp")
Bruno Muniz's avatar
Bruno Muniz committed
      sftpService.uploadBackup(url: url) {
        switch $0 {
        case .success(let backup):
          self.settings.value.backups[.sftp] = backup
        case .failure(let error):
          print(error.localizedDescription)
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }