mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-26 01:27:42 +00:00
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
added default and min size for config view
242 lines
8.2 KiB
Swift
242 lines
8.2 KiB
Swift
/*
|
|
* Keyman is copyright (C) SIL Global. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2026-06-30
|
|
*
|
|
* Tracks the state of a package being installed with functions
|
|
* to derive its temporary install location, compare it to a
|
|
* package of the same type if it exists and replace or delete depending
|
|
* on its version and user feedback.
|
|
*/
|
|
|
|
import Foundation
|
|
|
|
@MainActor // run on the main actor as it is called from SettingsContainer
|
|
public class PackageInstallHelper: Identifiable {
|
|
public let id = UUID()
|
|
// public var installationError: LocalizedError?
|
|
// public var errorMessage: String?
|
|
let temporaryKmpFileLocation: URL
|
|
let temporaryPackageLocation: URL
|
|
let installPackageLocation: URL
|
|
let installedPackages: [KeymanPackage] // needed to check for existing package after download
|
|
let isDownload: Bool // if not download, then the package was opened from disk or dropped
|
|
public private(set) var packageToInstall: KeymanPackage? // the newly downloaded package
|
|
var packageToReplace: KeymanPackage? // the package to replace, if it exists
|
|
|
|
public var packageName: String? {
|
|
return packageToInstall?.packageName
|
|
}
|
|
|
|
fileprivate let packageRepository: PackageRepo
|
|
|
|
public init(filename: String, packageName: String, packageRepo: PackageRepo, installedPackages: [KeymanPackage], isDownload: Bool) {
|
|
self.packageRepository = packageRepo
|
|
self.temporaryKmpFileLocation = self.packageRepository.getDownloadUrl(for: filename)
|
|
self.temporaryPackageLocation = self.packageRepository.getUnzipDestinationUrl(for: packageName)
|
|
self.installPackageLocation = self.packageRepository.buildInstallationUrlForPackageName(packageName: packageName)
|
|
self.installedPackages = installedPackages
|
|
self.isDownload = isDownload
|
|
|
|
// cannot be initialized until after download when packageName of new package is known
|
|
self.packageToReplace = nil
|
|
|
|
// if any packages are remaining from an earlier download, delete them
|
|
self.packageRepository.cleanupTempDirectory()
|
|
}
|
|
|
|
/**
|
|
* Indicates that a package has been downloaded and can be prepared for installation
|
|
*/
|
|
public func packageDownloadComplete(for kmpFileUrl: URL) throws {
|
|
print ("packageDownloadComplete \(kmpFileUrl)")
|
|
|
|
try self.prepareToInstall(for: kmpFileUrl)
|
|
}
|
|
|
|
/**
|
|
* Indicates that a package is ready to be unzipped and loaded
|
|
*/
|
|
public func prepareToInstall(for kmpFileUrl: URL) throws {
|
|
print ("prepareToInstall \(kmpFileUrl)")
|
|
|
|
do {
|
|
try self.unzipPackage(for: kmpFileUrl)
|
|
} catch {
|
|
self.cleanupFailedInstallation()
|
|
print ("package installation failed with error '\(error)' for \(kmpFileUrl)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that a package is ready to be unzipped and installed
|
|
*/
|
|
public func install() throws {
|
|
print ("install \(self.packageToInstall?.packageName ?? "unknown package")")
|
|
|
|
do {
|
|
try self.handleNewPackage()
|
|
} catch {
|
|
self.cleanupFailedInstallation()
|
|
print ("package installation failed with error '\(self.packageToInstall?.packageName ?? "unknown package")")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unzip and load the downloaded package
|
|
*/
|
|
func unzipPackage(for kmpFileUrl: URL) throws {
|
|
try self.packageRepository.unzipKmpFile(at: kmpFileUrl, to: self.temporaryPackageLocation)
|
|
|
|
// load the unzipped package from the temporary location and save a reference to it
|
|
let newPackage = try self.packageRepository.loadSinglePackage(packageUrl: self.temporaryPackageLocation)
|
|
self.packageToInstall = newPackage
|
|
}
|
|
|
|
/**
|
|
* Decides whether the package should be installed.
|
|
* - If this package is not replacing a package, then it is installed.
|
|
* - If this package is replacing an older package, the new package replaces the old.
|
|
* - If this package is replacing a newer package, then the user is notified to confirm.
|
|
*/
|
|
func handleNewPackage() throws {
|
|
// first check whether this install is replacing an existing package,
|
|
if self.checkForExistingPackage() {
|
|
if self.replacingInstalledPackageWithEarlierVersion() {
|
|
// check with the user before allowing a downgrade
|
|
self.sendNotificationToConfirmPackageDowngrade()
|
|
} else {
|
|
try self.replaceExistingPackageWithNewPackage()
|
|
}
|
|
} else {
|
|
try self.installNewPackage()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether a package of the same name is already installed which may be replaced.
|
|
*/
|
|
func checkForExistingPackage() -> Bool {
|
|
var packageExists = false
|
|
|
|
if let package = self.installedPackages.first(where: { $0.packageName == self.packageToInstall?.packageName }) {
|
|
self.packageToReplace = package
|
|
packageExists = true
|
|
}
|
|
return packageExists
|
|
}
|
|
|
|
/**
|
|
* Send a notification that an attempt to downgrade a package has been detected
|
|
*/
|
|
func sendNotificationToConfirmPackageDowngrade() {
|
|
NotificationCenter.default.post(name: .packageDowngradeRequested, object: nil)
|
|
}
|
|
|
|
/**
|
|
* Install the newly downloaded package (no existing package to replace)
|
|
*/
|
|
func installNewPackage() throws {
|
|
try self.movePackageFromTemporaryToInstalled()
|
|
if (self.isDownload) {
|
|
do {
|
|
try self.deleteDownloadedKmpFile()
|
|
} catch {
|
|
print("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)")
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.post(name: .newPackageInstalled, object: nil)
|
|
}
|
|
|
|
/**
|
|
* Replace the existing installed package with the newly download package
|
|
*/
|
|
func replaceExistingPackageWithNewPackage() throws {
|
|
try self.deleteInstalledPackage()
|
|
try self.deleteDownloadedKmpFile()
|
|
try self.movePackageFromTemporaryToInstalled()
|
|
|
|
NotificationCenter.default.post(name: .packageReplaced, object: nil)
|
|
}
|
|
|
|
/**
|
|
* Clean up the downloaded .kmp file and package folder
|
|
*/
|
|
func cleanupFailedInstallation() {
|
|
// we only have a .kmp file in the temp directory for downloads
|
|
if (self.isDownload) {
|
|
do {
|
|
try self.deleteDownloadedKmpFile()
|
|
} catch {
|
|
print("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)")
|
|
}
|
|
}
|
|
do {
|
|
try self.deleteUnzippedPackage()
|
|
} catch {
|
|
print("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryPackageLocation.lastPathComponent)")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the existing installed package that matches the downloaded package
|
|
*/
|
|
func deleteInstalledPackage() throws {
|
|
try FileManager.default.removeItem(at: self.installPackageLocation)
|
|
}
|
|
|
|
/**
|
|
* Move the downloaded package into the keyman packages directory.
|
|
*/
|
|
func movePackageFromTemporaryToInstalled() throws {
|
|
try FileManager.default.moveItem(at: self.temporaryPackageLocation, to: self.installPackageLocation)
|
|
|
|
// Update the KeymanPackage object with its new location
|
|
if let package = self.packageToInstall {
|
|
package.sourceDirectoryUrl = self.installPackageLocation
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the downloaded .kmp file from the temp directory
|
|
*/
|
|
func deleteDownloadedKmpFile() throws {
|
|
try FileManager.default.removeItem(at: self.temporaryKmpFileLocation)
|
|
}
|
|
|
|
/**
|
|
* Delete the unzipped package in the temp directory
|
|
*/
|
|
func deleteUnzippedPackage() throws {
|
|
try FileManager.default.removeItem(at: self.temporaryPackageLocation)
|
|
}
|
|
|
|
/**
|
|
* Determine whether the new package is older than the currently installed package
|
|
*/
|
|
func replacingInstalledPackageWithEarlierVersion() -> Bool {
|
|
var downgrade = false
|
|
|
|
guard let installedVersion = self.packageToReplace?.packageVersion,
|
|
let newVersion = self.packageToInstall?.packageVersion else {
|
|
return false
|
|
}
|
|
|
|
let comparisonResult = newVersion.compare(installedVersion, options: .numeric)
|
|
|
|
if comparisonResult == .orderedAscending {
|
|
// downgrade detected
|
|
downgrade = true
|
|
print("downgrade: new version is older than installed version")
|
|
} else if comparisonResult == .orderedDescending {
|
|
print("upgrade: new version is newer than installed version")
|
|
} else {
|
|
print("new and installed versions are identical")
|
|
}
|
|
|
|
return downgrade
|
|
}
|
|
}
|