Newer
Older
import DependencyInjection
import CloudFiles
import CloudFilesSFTP
import CloudFilesDrive
import CloudFilesICloud
import CloudFilesDropbox
import KeychainAccess
public enum BackupProvider: Equatable, Codable {
case sftp
case drive
case icloud
case dropbox
}
@Dependency var messenger: Messenger
@Dependency var networkManager: NetworkMonitoring
@KeyObject(.email, defaultValue: nil) var email: String?
@KeyObject(.phone, defaultValue: nil) var phone: String?
@KeyObject(.username, defaultValue: nil) var username: String?
@KeyObject(.backupSettings, defaultValue: nil) var storedSettings: Data?
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)
}
public func setupSFTP(host: String, username: String, password: String) {
managers[.sftp] = .sftp(
host: host,
username: username,
password: password,
fileName: "backup.xxm"
)
refreshBackups()
refreshConnections()
}
public func stopBackups() {
if messenger.isBackupRunning() == true {
try! messenger.stopBackup()
public func initializeBackup(passphrase: String) {
try! messenger.startBackup(
password: passphrase,
params: .init(
username: username!,
email: email,
phone: phone
)
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 { return }
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: BackupProvider, enabling: Bool) {
settings.value.enabledService = enabling ? service : nil
}
public func authorize(
service: BackupProvider,
presenting screen: UIViewController
) {
do {
try managers[service]?.link(screen) { [weak self] in
guard let self else { return }
switch $0 {
case .success:
case .failure(let error):
print(error.localizedDescription)
}
} catch {
print(error.localizedDescription)
extension BackupService {
private func refreshConnections() {
managers.forEach { provider, manager in
if manager.isLinked() && !settings.value.connectedServices.contains(provider) {
settings.value.connectedServices.insert(provider)
} else if !manager.isLinked() && settings.value.connectedServices.contains(provider) {
settings.value.connectedServices.remove(provider)
managers.forEach { provider, manager in
if manager.isLinked() {
do {
try manager.fetch { [weak self] in
guard let self else { return }
switch $0 {
case .success(let metadata):
self.settings.value.backups[provider] = metadata
case .failure(let error):
print(error.localizedDescription)
}
}
} catch {
print(error.localizedDescription)
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, options: .atomic)
} catch {
print(">>> Couldn't write to temp: \(error.localizedDescription)")
return
}
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")
managers[.sftp] = .sftp(
host: host,
username: username,
password: password,
fileName: "backup.xxm"
)
}
if let manager = managers[enabledService] {
do {
try manager.upload(data) { [weak self] in
guard let self else { return }
switch $0 {
case .success(let metadata):
self.settings.value.backups[enabledService] = .init(
size: metadata.size,
lastModified: metadata.lastModified
)
case .failure(let error):
print(error.localizedDescription)
}
} catch {
print(error.localizedDescription)