Skip to content
Snippets Groups Projects
BackupService.swift 6.95 KiB
Newer Older
import UIKit
import Models
import Combine
import XXClient
import Defaults
import CloudFiles
import CloudFilesSFTP
import NetworkMonitor
import KeychainAccess
import XXMessengerClient
import DependencyInjection

public final class BackupService {
Bruno Muniz's avatar
Bruno Muniz committed
  @Dependency var messenger: Messenger
  @Dependency var networkManager: NetworkMonitoring

  @KeyObject(.email, defaultValue: nil) var email: String?
  @KeyObject(.phone, defaultValue: nil) var phone: String?
Bruno Muniz's avatar
Bruno Muniz committed
  @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<CloudSettings, Never> {
Bruno Muniz's avatar
Bruno Muniz committed
    settings.handleEvents(receiveSubscription: { [weak self] _ in
      guard let self = self else { return }
      self.settings.value.connectedServices = CloudFilesManager.all.linkedServices()
      CloudFilesManager.all.lastBackups { [weak self] in
        guard let self else { return }
        self.settings.value.backups = $0
      }
Bruno Muniz's avatar
Bruno Muniz committed
    }).eraseToAnyPublisher()
  }

  private var connType: ConnectionType = .wifi
  private var cancellables = Set<AnyCancellable>()
  private lazy var settings = CurrentValueSubject<CloudSettings, Never>(.init(fromData: storedSettings))
Bruno Muniz's avatar
Bruno Muniz committed

  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)
  }
  func didSetWiFiOnly(enabled: Bool) {
    settings.value.wifiOnlyBackup = enabled
  func didSetAutomaticBackup(enabled: Bool) {
    settings.value.automaticBackups = enabled
    shouldBackupIfSetAutomatic()
Bruno Muniz's avatar
Bruno Muniz committed
  }
  func toggle(service: CloudService, enabling: Bool) {
    settings.value.enabledService = enabling ? service : nil
Bruno Muniz's avatar
Bruno Muniz committed
  }
  func didForceBackup() {
    if let lastBackup = try? Data(contentsOf: getBackupURL()) {
      performUpload(of: lastBackup)
    }
Bruno Muniz's avatar
Bruno Muniz committed
  }
  public func didUpdateFacts() {
    storeFacts()
Bruno Muniz's avatar
Bruno Muniz committed
  }
  public func updateLocalBackup(_ data: Data) {
Bruno Muniz's avatar
Bruno Muniz committed
    do {
      try data.write(to: getBackupURL())
      shouldBackupIfSetAutomatic()
Bruno Muniz's avatar
Bruno Muniz committed
    } catch {
      fatalError("Couldn't write backup to fileurl")
    }
  private func shouldBackupIfSetAutomatic() {
    guard let lastBackup = try? Data(contentsOf: getBackupURL()) else {
      print(">>> No stored backup so won't upload anything.")
      return
    }
    guard settings.value.automaticBackups else {
      print(">>> Backups are not set to automatic")
      return
    }
    guard settings.value.enabledService != nil else {
      print(">>> No service enabled to upload")
      return
    }
    if settings.value.wifiOnlyBackup {
      guard connType == .wifi else {
        print(">>> WiFi only backups, and connType != Wifi")
        return
      }
Bruno Muniz's avatar
Bruno Muniz committed
    } else {
      guard connType != .unknown else {
        print(">>> Connectivity is unknown")
        return
      }
    performUpload(of: lastBackup)
  }
  // MARK: - Messenger

  func initializeBackup(passphrase: String) {
    do {
      try messenger.startBackup(
        password: passphrase,
        params: .init(
          username: username!,
          email: email,
          phone: phone
        )
      )
    } catch {
      print(">>> Exception when calling `messenger.startBackup`: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
    }
Bruno Muniz's avatar
Bruno Muniz committed

  func stopBackups() {
    if messenger.isBackupRunning() == true {
      do {
        try messenger.stopBackup()
      } catch {
        print(">>> Exception when calling `messenger.stopBackup`: \(error.localizedDescription)")
      }
    }
  func storeFacts() {
    var facts: [String: String] = [:]
    facts["username"] = username!
    facts["email"] = email
    facts["phone"] = phone
    facts["timestamp"] = "\(Date.asTimestamp)"
    guard let backupManager = messenger.backup.get() else {
      print(">>> Tried to store facts in JSON but there's no backup manager instance")
      return
    }
    guard let data = try? JSONSerialization.data(withJSONObject: facts) else {
      print(">>> Tried to generate data with json dictionary but failed")
      return
    }
    guard let string = String(data: data, encoding: .utf8) else {
      print(">>> Tried to extract string from json dict object but failed")
      return
    }
    backupManager.addJSON(string)
  // MARK: - CloudProviders
  func setupSFTP(host: String, username: String, password: String) {
    let sftpManager = CloudFilesManager.sftp(
      host: host,
      username: username,
      password: password,
      fileName: "backup.xxm"
    )
    CloudFilesManager.all[.sftp] = sftpManager
      try sftpManager.fetch {
        case .success(let metadata):
          self.settings.value.backups[.sftp] = metadata
        case .failure(let error):
          print(">>> Error fetching sftp: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
      }
      print(">>> Exception fetching sftp: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
  }
  func authorize(
    service: CloudService,
    presenting screen: UIViewController
  ) {
    service.authorize(presenting: screen) {
      switch $0 {
      case .success:
        self.settings.value.connectedServices = CloudFilesManager.all.linkedServices()
        CloudFilesManager.all.lastBackups { [weak self] in
          guard let self else { return }
          self.settings.value.backups = $0
      case .failure(let error):
        print(">>> Tried to authorize \(service) but failed: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }
  func performUpload(of data: 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
    }
    if enabledService == .sftp {
      let keychain = Keychain(service: "SFTP-XXM")
      guard let host = try? keychain.get("host"),
            let password = try? keychain.get("pwd"),
            let username = try? keychain.get("username") else {
        fatalError(">>> Tried to perform an sftp backup but its not configured")
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed

      CloudFilesManager.all[.sftp] = .sftp(
        host: host,
        username: username,
        password: password,
        fileName: "backup.xxm"
      )
    }
Bruno Muniz's avatar
Bruno Muniz committed

    enabledService.backup(data: data) {
      switch $0 {
      case .success(let metadata):
        self.settings.value.backups[enabledService] = .init(
          size: metadata.size,
          lastModified: metadata.lastModified
        )
      case .failure(let error):
        print(">>> Failed to perform a backup upload: \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed
  }

  private func getBackupURL() -> URL {
    guard let folderURL = try? FileManager.default.url(
      for: .applicationSupportDirectory,
      in: .userDomainMask,
      appropriateFor: nil,
      create: true
    ) else { fatalError(">>> Couldn't generate the URL for backup") }

    return folderURL
      .appendingPathComponent("backup")
      .appendingPathExtension("xxm")
  }