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

  @KeyObject(.username, defaultValue: nil) var username: String?
  @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 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()
    }
  }

  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() {
    if icloudService.isAuthorized() {
      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() {
      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() {
      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
      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:
      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)
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }