mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-08-04 13:07:04 -04:00
220 lines
8.9 KiB
Swift
220 lines
8.9 KiB
Swift
// This file is part of Kiwix for iOS & macOS.
|
|
//
|
|
// Kiwix is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 3 of the License, or
|
|
// any later version.
|
|
//
|
|
// Kiwix is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
|
|
|
#if canImport(BackgroundTasks)
|
|
import BackgroundTasks
|
|
#endif
|
|
import CoreData
|
|
import os
|
|
|
|
import Defaults
|
|
|
|
struct LibraryOperations {
|
|
private init() {}
|
|
|
|
// MARK: - Open
|
|
|
|
/// Open a zim file with url
|
|
/// - Parameter url: url of the zim file
|
|
@discardableResult
|
|
static func open(url: URL, onComplete: (() -> Void)? = nil) async -> ZimFileMetaData? {
|
|
guard let metadata = await ZimFileService.getMetaData(url: url),
|
|
let fileURLBookmark = await ZimFileService.getFileURLBookmarkData(for: url) else { return nil }
|
|
|
|
// open the file
|
|
do {
|
|
try await ZimFileService.shared.open(fileURLBookmark: fileURLBookmark, for: metadata.fileID)
|
|
} catch {
|
|
return nil
|
|
}
|
|
|
|
// upsert zim file in the database
|
|
Database.shared.performBackgroundTask { context in
|
|
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
|
let predicate = NSPredicate(format: "fileID == %@", metadata.fileID as CVarArg)
|
|
let fetchRequest = ZimFile.fetchRequest(predicate: predicate)
|
|
guard let zimFile = try? context.fetch(fetchRequest).first ?? ZimFile(context: context) else { return }
|
|
LibraryOperations.configureZimFile(zimFile, metadata: metadata)
|
|
zimFile.fileURLBookmark = fileURLBookmark
|
|
zimFile.isMissing = false
|
|
if context.hasChanges { try? context.save() }
|
|
Task {
|
|
await MainActor.run {
|
|
onComplete?()
|
|
}
|
|
}
|
|
}
|
|
|
|
return metadata
|
|
}
|
|
|
|
/// Reopen zim files from url bookmark data.
|
|
static func reopen() async {
|
|
var successCount = 0
|
|
let context = Database.shared.viewContext
|
|
let request = ZimFile.fetchRequest(predicate: ZimFile.Predicate.isDownloaded)
|
|
|
|
guard let zimFiles = try? context.fetch(request) else {
|
|
return
|
|
}
|
|
|
|
for zimFile in zimFiles {
|
|
guard let data = zimFile.fileURLBookmark else { return }
|
|
|
|
do {
|
|
if let data = try await ZimFileService.shared.open(fileURLBookmark: data, for: zimFile.fileID) {
|
|
zimFile.fileURLBookmark = data
|
|
}
|
|
zimFile.isMissing = false
|
|
successCount += 1
|
|
} catch ZimFileOpenError.missing {
|
|
zimFile.isMissing = true
|
|
} catch {
|
|
zimFile.fileURLBookmark = nil
|
|
zimFile.isMissing = false
|
|
}
|
|
}
|
|
Task { @MainActor in
|
|
if context.hasChanges {
|
|
try? context.save()
|
|
}
|
|
}
|
|
os_log("Reopened %d out of %d zim files", log: Log.LibraryOperations, type: .info, successCount, zimFiles.count)
|
|
}
|
|
|
|
/// Scan a directory and open available zim files inside it
|
|
/// - Parameter url: directory to scan
|
|
static func scanDirectory(_ url: URL) {
|
|
guard let fileURLs = try? FileManager.default.contentsOfDirectory(
|
|
at: url,
|
|
includingPropertiesForKeys: nil,
|
|
options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants]
|
|
).filter({ $0.pathExtension == "zim"}) else { return }
|
|
os_log("Discovered %d probable zim files.", log: Log.LibraryOperations, type: .info, fileURLs.count)
|
|
Task {
|
|
for fileURL in fileURLs {
|
|
await LibraryOperations.open(url: fileURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Configure
|
|
|
|
/// Configure a zim file object based on its metadata.
|
|
static func configureZimFile(_ zimFile: ZimFile, metadata: ZimFileMetaData) {
|
|
zimFile.articleCount = metadata.articleCount.int64Value
|
|
zimFile.category = (Category(rawValue: metadata.category) ?? .other).rawValue
|
|
zimFile.created = metadata.creationDate
|
|
zimFile.fileDescription = metadata.fileDescription
|
|
zimFile.fileID = metadata.fileID
|
|
zimFile.flavor = metadata.flavor
|
|
zimFile.hasDetails = metadata.hasDetails
|
|
zimFile.hasPictures = metadata.hasPictures
|
|
zimFile.hasVideos = metadata.hasVideos
|
|
zimFile.languageCode = metadata.languageCodes
|
|
zimFile.mediaCount = metadata.mediaCount.int64Value
|
|
zimFile.name = metadata.title
|
|
zimFile.persistentID = metadata.groupIdentifier
|
|
zimFile.requiresServiceWorkers = metadata.requiresServiceWorkers
|
|
zimFile.size = metadata.size.int64Value
|
|
|
|
// Overwrite these, only if there are new values
|
|
if let faviconURL = metadata.faviconURL { zimFile.faviconURL = faviconURL }
|
|
if let faviconData = metadata.faviconData { zimFile.faviconData = faviconData }
|
|
if let downloadURL = metadata.downloadURL { zimFile.downloadURL = downloadURL }
|
|
}
|
|
|
|
// MARK: - Deletion
|
|
|
|
/// Unlink a zim file from library, delete associated bookmarks, and delete the file.
|
|
/// - Parameter zimFile: the zim file to delete
|
|
@ZimActor static func delete(zimFileID: UUID) {
|
|
guard let url = ZimFileService.shared.getFileURL(zimFileID: zimFileID) else { return }
|
|
defer { try? FileManager.default.removeItem(at: url) }
|
|
LibraryOperations.unlink(zimFileID: zimFileID)
|
|
}
|
|
|
|
/// Unlink a zim file from library, delete associated bookmarks, but don't delete the file.
|
|
/// - Parameter zimFile: the zim file to unlink
|
|
@ZimActor static func unlink(zimFileID: UUID) {
|
|
ZimFileService.shared.close(fileID: zimFileID)
|
|
Database.shared.performBackgroundTask { context in
|
|
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
|
guard let zimFile = try? ZimFile.fetchRequest(fileID: zimFileID).execute().first else { return }
|
|
zimFile.bookmarks.forEach { context.delete($0) }
|
|
if zimFile.downloadURL == nil {
|
|
context.delete(zimFile)
|
|
} else {
|
|
zimFile.fileURLBookmark = nil
|
|
zimFile.isMissing = false
|
|
}
|
|
zimFile.tabs.forEach { context.delete($0) }
|
|
|
|
if let tabs = try? Tab.fetchRequest().execute() {
|
|
let tabIds = tabs.map { $0.objectID }
|
|
// clear out all the browserViewModels of tabs no longer in use
|
|
BrowserViewModel.keepOnlyTabsByIds(Set(tabIds))
|
|
|
|
#if os(iOS)
|
|
// make sure we won't end up without any tabs
|
|
if tabs.count == 0 {
|
|
let tab = Tab(context: context)
|
|
tab.created = Date()
|
|
tab.lastOpened = Date()
|
|
try? context.obtainPermanentIDs(for: [tab])
|
|
}
|
|
#else
|
|
if context.hasChanges { try? context.save() }
|
|
Task { @MainActor in
|
|
NotificationCenter.keepOnlyTabs(Set(tabIds))
|
|
}
|
|
#endif
|
|
}
|
|
if context.hasChanges { try? context.save() }
|
|
}
|
|
}
|
|
|
|
// MARK: - Backup
|
|
|
|
/// Apply iCloud backup setting on zim files in document directory.
|
|
/// - Parameter isEnabled: if file should be included in backup
|
|
static func applyFileBackupSetting(isEnabled: Bool? = nil) {
|
|
do {
|
|
let directory = try FileManager.default.url(
|
|
for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false
|
|
)
|
|
let urls = try FileManager.default.contentsOfDirectory(
|
|
at: directory,
|
|
includingPropertiesForKeys: [.isExcludedFromBackupKey],
|
|
options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants]
|
|
).filter({ $0.pathExtension.contains("zim") })
|
|
let backupDocumentDirectory = isEnabled ?? Defaults[.backupDocumentDirectory]
|
|
try urls.forEach { url in
|
|
var resourceValues = URLResourceValues()
|
|
resourceValues.isExcludedFromBackup = !backupDocumentDirectory
|
|
var url = url
|
|
try url.setResourceValues(resourceValues)
|
|
}
|
|
os_log(
|
|
"Applying zim file backup setting (%s) on %u zim file(s).",
|
|
log: Log.LibraryOperations,
|
|
type: .info,
|
|
backupDocumentDirectory ? "backing up" : "not backing up",
|
|
urls.count
|
|
)
|
|
} catch {}
|
|
}
|
|
}
|