kiwix-apple/SwiftUI/Model/ZimMigration.swift
2024-07-24 00:30:50 +02:00

115 lines
4.0 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/.
//
// ZimMigration.swift
// Kiwix
import Foundation
import CoreData
enum ZimMigration {
/// Holds the new zimfile host:
/// Set during migration,
/// and read back when updating URLS mapped from WebView interaction state,
/// witch is saved as Data for each opened Tab
@MainActor private static var newHost: String?
private static let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)]
private static let requestLatestZimFile = ZimFile.fetchRequest(
predicate: ZimFile.Predicate.isDownloaded,
sortDescriptors: Self.sortDescriptors
)
static func forCustomApps() {
guard FeatureFlags.hasLibrary == false else { return }
Database.shared.performBackgroundTask { context in
guard var zimFiles = try? requestLatestZimFile.execute(),
zimFiles.count > 1,
let latest = zimFiles.popLast() else {
return
}
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
for zimFile in zimFiles {
migrateFrom(zimFile: zimFile, toZimFile: latest, using: context)
}
}
}
/// Migrates the bookmars from an old to new zim file,
/// also updates the bookmark urls accordingly (based on the new zim id as the host of those URLs)
/// deletes the old zim file in the DB
private static func migrateFrom(
zimFile fromZim: ZimFile,
toZimFile toZim: ZimFile,
using context: NSManagedObjectContext
) {
let newHost = toZim.fileID.uuidString
Task {
await MainActor.run {
Self.newHost = newHost
}
}
fromZim.bookmarks.forEach { (bookmark: Bookmark) in
bookmark.zimFile = toZim
if let newArticleURL = bookmark.articleURL.updateHost(to: newHost) {
bookmark.articleURL = newArticleURL
}
}
fromZim.tabs.forEach { (tab: Tab) in
tab.zimFile = toZim
tab.interactionState = tab.interactionState?.updateHost(to: newHost)
}
context.delete(fromZim)
if context.hasChanges { try? context.save() }
}
@MainActor
private static func latestZimFileHost() async -> String {
if let newHost = Self.newHost { return newHost }
// if it wasn't set before, set and return by the last ZimFile in DB:
guard let zimFile = try? Database.shared.viewContext.fetch(requestLatestZimFile).first else {
fatalError("we should have at least 1 zim file for a custom app")
}
let newHost = zimFile.fileID.uuidString
// save the new host for later
Self.newHost = newHost
return newHost
}
}
extension URL {
func updateHost(to newHost: String) -> URL? {
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return nil }
components.host = newHost
return components.url
}
}
extension Data {
func updateHost(to newHost: String) -> Data {
let string = String(decoding: self, as: UTF8.self)
if let replaced = try? string.replacingRegex(
matching: "kiwix:\\/\\/[A-Z0-9-]{0,36}\\/",
with: "kiwix://\(newHost)/"
) {
return Data(replaced.utf8)
} else {
return self
}
}
}