Public should not be used in an app since it declares public to additional modules. However, an app is one module. Some structs/ classes need to be left public to conform to CoreData's generation. Signed-off-by: kingbri <bdashore3@proton.me>
245 lines
9.1 KiB
Swift
245 lines
9.1 KiB
Swift
//
|
|
// BackupManager.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 9/16/22.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
class BackupManager: ObservableObject {
|
|
// Constant variable for backup versions
|
|
private let latestBackupVersion: Int = 2
|
|
|
|
var logManager: LoggingManager?
|
|
|
|
@Published var showRestoreAlert = false
|
|
@Published var showRestoreCompletedAlert = false
|
|
@Published var restoreCompletedMessage: [String] = []
|
|
|
|
@Published var backupUrls: [URL] = []
|
|
@Published var selectedBackupUrl: URL?
|
|
|
|
@MainActor
|
|
private func updateRestoreCompletedMessage(newString: String) {
|
|
restoreCompletedMessage.append(newString)
|
|
}
|
|
|
|
@MainActor
|
|
private func toggleRestoreCompletedAlert() {
|
|
showRestoreCompletedAlert.toggle()
|
|
}
|
|
|
|
@MainActor
|
|
private func updateBackupUrls(newUrl: URL) {
|
|
backupUrls.append(newUrl)
|
|
}
|
|
|
|
func createBackup() async {
|
|
var backup = Backup(version: latestBackupVersion)
|
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
|
|
|
let bookmarkRequest = Bookmark.fetchRequest()
|
|
if let fetchedBookmarks = try? backgroundContext.fetch(bookmarkRequest) {
|
|
backup.bookmarks = fetchedBookmarks.compactMap {
|
|
BookmarkJson(
|
|
title: $0.title,
|
|
source: $0.source,
|
|
size: $0.size,
|
|
magnetLink: $0.magnetLink,
|
|
magnetHash: $0.magnetHash,
|
|
seeders: $0.seeders,
|
|
leechers: $0.leechers
|
|
)
|
|
}
|
|
}
|
|
|
|
let historyRequest = History.fetchRequest()
|
|
if let fetchedHistory = try? backgroundContext.fetch(historyRequest) {
|
|
backup.history = fetchedHistory.compactMap { history in
|
|
if history.entries == nil {
|
|
return nil
|
|
} else {
|
|
return HistoryJson(
|
|
dateString: history.dateString,
|
|
date: history.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970,
|
|
entries: history.entryArray.compactMap { entry in
|
|
if let name = entry.name, let url = entry.url {
|
|
return HistoryEntryJson(
|
|
name: name,
|
|
subName: entry.subName,
|
|
url: url,
|
|
timeStamp: entry.timeStamp,
|
|
source: entry.source
|
|
)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
let sourceRequest = Source.fetchRequest()
|
|
if let sources = try? backgroundContext.fetch(sourceRequest) {
|
|
backup.sourceNames = sources.map(\.name)
|
|
}
|
|
|
|
let actionRequest = Action.fetchRequest()
|
|
if let actions = try? backgroundContext.fetch(actionRequest) {
|
|
backup.actionNames = actions.map(\.name)
|
|
}
|
|
|
|
let pluginListRequest = PluginList.fetchRequest()
|
|
if let pluginLists = try? backgroundContext.fetch(pluginListRequest) {
|
|
backup.pluginListUrls = pluginLists.map(\.urlString)
|
|
}
|
|
|
|
do {
|
|
let encodedJson = try JSONEncoder().encode(backup)
|
|
let backupsPath = FileManager.default.appDirectory.appendingPathComponent("Backups")
|
|
if !FileManager.default.fileExists(atPath: backupsPath.path) {
|
|
try FileManager.default.createDirectory(atPath: backupsPath.path, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
|
|
let snapshot = Int(Date().timeIntervalSince1970.rounded())
|
|
let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb")
|
|
|
|
try encodedJson.write(to: writeUrl)
|
|
|
|
await updateBackupUrls(newUrl: writeUrl)
|
|
} catch {
|
|
await logManager?.error("Backup: \(error)")
|
|
}
|
|
}
|
|
|
|
// Backup is in local documents directory, so no need to restore it from the shared URL
|
|
// Pass the pluginManager reference since it's not used throughout the class like logManager
|
|
func restoreBackup(pluginManager: PluginManager, doOverwrite: Bool) async {
|
|
guard let backupUrl = selectedBackupUrl else {
|
|
await logManager?.error(
|
|
"Backup restore: Could not find backup in app directory.",
|
|
description: "Could not find the selected backup in the local directory."
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
|
|
|
do {
|
|
// Delete all relevant entities to prevent issues with restoration if overwrite is selected
|
|
if doOverwrite {
|
|
try PersistenceController.shared.batchDelete("Bookmark")
|
|
try PersistenceController.shared.batchDelete("History")
|
|
try PersistenceController.shared.batchDelete("HistoryEntry")
|
|
try PersistenceController.shared.batchDelete("PluginList")
|
|
try PersistenceController.shared.batchDelete("Source")
|
|
try PersistenceController.shared.batchDelete("Action")
|
|
}
|
|
|
|
let file = try Data(contentsOf: backupUrl)
|
|
|
|
let backup = try JSONDecoder().decode(Backup.self, from: file)
|
|
|
|
if let bookmarks = backup.bookmarks {
|
|
for bookmark in bookmarks {
|
|
PersistenceController.shared.createBookmark(bookmark, performSave: false)
|
|
}
|
|
}
|
|
|
|
if let storedHistories = backup.history {
|
|
for storedHistory in storedHistories {
|
|
for storedEntry in storedHistory.entries {
|
|
PersistenceController.shared.createHistory(
|
|
storedEntry,
|
|
performSave: false,
|
|
isBackup: true,
|
|
date: storedHistory.date
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
let version = backup.version ?? -1
|
|
|
|
if let storedLists = backup.sourceLists, version < 2 {
|
|
// Only present in v1 or no version backups
|
|
for list in storedLists {
|
|
try await pluginManager.addPluginList(list.urlString, existingPluginList: nil)
|
|
}
|
|
} else if let pluginListUrls = backup.pluginListUrls {
|
|
// v2 and up
|
|
for listUrl in pluginListUrls {
|
|
try await pluginManager.addPluginList(listUrl, existingPluginList: nil)
|
|
}
|
|
}
|
|
|
|
if let sourceNames = backup.sourceNames {
|
|
await updateRestoreCompletedMessage(newString: sourceNames.isEmpty ? "No sources need to be reinstalled" : "Reinstall sources: \(sourceNames.joined(separator: ", "))")
|
|
}
|
|
|
|
if let actionNames = backup.actionNames {
|
|
await updateRestoreCompletedMessage(newString: actionNames.isEmpty ? "No actions need to be reinstalled" : "Reinstall actions: \(actionNames.joined(separator: ", "))")
|
|
}
|
|
|
|
PersistenceController.shared.save(backgroundContext)
|
|
|
|
await toggleRestoreCompletedAlert()
|
|
} catch {
|
|
await logManager?.error(
|
|
"Backup restore: \(error)",
|
|
description: "A backup restore error was logged"
|
|
)
|
|
}
|
|
}
|
|
|
|
// Remove the backup from files and then the list
|
|
// Removes an index if it's provided
|
|
func removeBackup(backupUrl: URL, index: Int?) {
|
|
do {
|
|
try FileManager.default.removeItem(at: backupUrl)
|
|
|
|
if let index {
|
|
backupUrls.remove(at: index)
|
|
} else {
|
|
backupUrls.removeAll(where: { $0 == backupUrl })
|
|
}
|
|
} catch {
|
|
Task {
|
|
await logManager?.error("Backup removal: \(error)")
|
|
print("Backup removal: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func copyBackup(backupUrl: URL) {
|
|
let backupSecured = backupUrl.startAccessingSecurityScopedResource()
|
|
|
|
defer {
|
|
if backupSecured {
|
|
backupUrl.stopAccessingSecurityScopedResource()
|
|
}
|
|
}
|
|
|
|
let backupsPath = FileManager.default.appDirectory.appendingPathComponent("Backups")
|
|
let localBackupPath = backupsPath.appendingPathComponent(backupUrl.lastPathComponent)
|
|
|
|
do {
|
|
if FileManager.default.fileExists(atPath: localBackupPath.path) {
|
|
try FileManager.default.removeItem(at: localBackupPath)
|
|
} else if !FileManager.default.fileExists(atPath: backupsPath.path) {
|
|
try FileManager.default.createDirectory(atPath: backupsPath.path, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
|
|
try FileManager.default.copyItem(at: backupUrl, to: localBackupPath)
|
|
|
|
selectedBackupUrl = localBackupPath
|
|
} catch {
|
|
Task {
|
|
await logManager?.error("Backup copy: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|