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

Bruno Muniz's avatar
Bruno Muniz committed
  public var backupsPublisher: AnyPublisher<[CloudService: Fetch.Metadata], Never> {
    backupSubject.eraseToAnyPublisher()
  }

  public var connectedServicesPublisher: AnyPublisher<Set<CloudService>, Never> {
    connectedServicesSubject.eraseToAnyPublisher()
  }

  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 }
Bruno Muniz's avatar
Bruno Muniz committed
      self.connectedServicesSubject.send(CloudFilesManager.all.linkedServices())
      self.fetchBackupOnAllProviders()
Bruno Muniz's avatar
Bruno Muniz committed
    }).eraseToAnyPublisher()
  }

  private var connType: ConnectionType = .wifi
  private var cancellables = Set<AnyCancellable>()
Bruno Muniz's avatar
Bruno Muniz committed
  private let connectedServicesSubject = CurrentValueSubject<Set<CloudService>, Never>([])
  private let backupSubject = CurrentValueSubject<[CloudService: Fetch.Metadata], Never>([:])
  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
Bruno Muniz's avatar
Bruno Muniz committed
      try sftpManager.fetch { [weak self] in
        guard let self else { return }
        case .success(let metadata):
Bruno Muniz's avatar
Bruno Muniz committed
          self.backupSubject.value[.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
  ) {
Bruno Muniz's avatar
Bruno Muniz committed
    guard let manager = CloudFilesManager.all[service] else {
      print(">>> Tried to link/auth but the enabled service is not set")
      return
    }
    do {
      try manager.link(screen) { [weak self] in
        guard let self else { return }
        switch $0 {
        case .success:
          self.connectedServicesSubject.value.insert(service)
          self.fetchBackupOnAllProviders()
        case .failure(let error):
          self.connectedServicesSubject.value.remove(service)
          print(">>> Failed to link/auth \(service): \(error.localizedDescription)")
Bruno Muniz's avatar
Bruno Muniz committed
      }
Bruno Muniz's avatar
Bruno Muniz committed
    } catch {
      print(">>> Exception trying to link/auth \(service): \(error.localizedDescription)")
    }
  }

  func fetchBackupOnAllProviders() {
    CloudFilesManager.all.lastBackups { [weak self] in
      guard let self else { return }
      self.backupSubject.send($0)
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
      }
      CloudFilesManager.all[.sftp] = .sftp(
        host: host,
        username: username,
        password: password,
        fileName: "backup.xxm"
      )
    }
Bruno Muniz's avatar
Bruno Muniz committed
    guard let manager = CloudFilesManager.all[enabledService] else {
      print(">>> Tried to upload but the enabled service is not set")
      return
    }
    do {
      try manager.upload(data) { [weak self] in
        guard let self else { return }
Bruno Muniz's avatar
Bruno Muniz committed

Bruno Muniz's avatar
Bruno Muniz committed
        switch $0 {
        case .success(let metadata):
          self.backupSubject.value[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
    } catch {
      print(">>> Exception performing a backup upload: \(error.localizedDescription)")
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")
  }