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
modifying the class to handle not just installation of packages being downloaded but also installation due to drag and drop or opening a .kmp from the file menu also added boolean to distinguish a download and install operation because that is the only case where we delete the original .kmp file that was downloaded to the temp directory
219 lines
7.6 KiB
Swift
219 lines
7.6 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 {
|
|
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
|
|
var packageToInstall: KeymanPackage? // the newly downloaded package
|
|
var packageToReplace: KeymanPackage? // the package to replace, if it exists
|
|
|
|
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 installed
|
|
*/
|
|
public func prepareToInstall(for kmpFileUrl: URL) throws {
|
|
print ("prepareToInstall \(kmpFileUrl)")
|
|
|
|
do {
|
|
try self.unzipDownloadedPackage(for: kmpFileUrl)
|
|
try self.handleNewPackage()
|
|
} catch {
|
|
self.cleanupFailedInstallation()
|
|
print ("package installation failed with error '\(error)' for \(kmpFileUrl)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unzip and load the downloaded package
|
|
*/
|
|
func unzipDownloadedPackage(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() {
|
|
print("cleanupFailedInstallation of: \(self.temporaryPackageLocation.lastPathComponent)")
|
|
do {
|
|
try self.deleteDownloadedKmpFile()
|
|
} catch {
|
|
print("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)")
|
|
}
|
|
do {
|
|
try self.deleteDownloadedPackage()
|
|
} 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 downloaded package from the temp directory
|
|
*/
|
|
func deleteDownloadedPackage() 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
|
|
}
|
|
}
|