spiegel-keyman/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift
Shawn Schantz a7bcceff47
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
feat(mac): add protocols, stubs and unit tests
added suite for Settings Container and stubs to
test it without accessing disk or user defaults
2026-05-04 11:38:56 -04:00

199 lines
6.6 KiB
Swift

/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2025-12-10
*
* PackageRepository is responsible for reading, writing and removing
* Keyman data on disk.
*
*/
import Foundation
public class PackageRepository: PackageRepo {
fileprivate let packageFileName = "kmp.json"
fileprivate let pathUtil: KeymanPaths
public init() {
self.pathUtil = KeymanPaths()
}
/**
* after reading the Keyman packages from disk, wrap each package as a `KeymanPackage` object
*/
public func loadPackages() -> [KeymanPackage] {
var installedPackages: [KeymanPackage] = []
let packageSourceArray = self.readKeymanPackagesForKeyman19()
// create a KeymanPackage object for each PackageSource object and insert it in the array if it is valid
for source in packageSourceArray {
let package = KeymanPackage(packageSource: source)
if (package.validate()) {
installedPackages.append(package)
} else {
print("package '\(source.packageName)' is not valid")
}
}
return installedPackages
}
/**
* delete the package from disk
*/
public func deletePackage(package: KeymanPackage) {
print("deleting package: \(package.sourceDirectoryUrl)")
do {
try FileManager.default.removeItem(at: package.sourceDirectoryUrl)
print("deleted package: \(package.sourceDirectoryUrl)")
} catch {
print("could not delete directory: \(error.localizedDescription)")
}
}
/**
* Creates the directory tree where keyboards are stored under the standard 'Group Containers' directory
*/
public func createKeyman19SharedDataDirectories() {
if let keyboardDirectory = pathUtil.keyman19KeyboardsDirectory {
do {
// create the directory if it doesn't already exist
if !FileManager.default.fileExists(atPath: keyboardDirectory.path) {
try FileManager.default.createDirectory(at: keyboardDirectory, withIntermediateDirectories: true, attributes: nil)
// ConfigLogger.shared.testLogger.debug("Created directory: \(keyboardDirectory.path)")
print("Created directory: \(keyboardDirectory.path)")
} else {
print("Directory already exists: \(keyboardDirectory.path)")
}
} catch {
print("Error creating directory: \(error.localizedDescription)")
}
} else {
print("Unable to create directory, Group Container URL not found.")
}
}
// TODO: delete
/**
* for group container testing purposes to check directory access
*/
func writeTestFileToContainer() {
if let keyboardDirectory = self.pathUtil.keyman19KeyboardsDirectory {
print("About to write to: \(keyboardDirectory.path)")
do {
// Example: Save a file to the shared directory
let fileURL = keyboardDirectory.appendingPathComponent("bogus-kmp.txt")
let data = "Not a .kmp file".data(using: .utf8)
try data?.write(to: fileURL)
print("Wrote data to shared file: \(fileURL.path)")
} catch {
print("Error writing file to shared directory: \(error.localizedDescription)")
}
} else {
print("App Group container URL not found. Check your entitlements and provisioning profiles.")
}
}
/**
* Check to see whether the shared Keyman data directory exists under 'Library/Group Containers/'
*/
public func keyman19SharedDataDirectoryExists() -> Bool {
guard let keyboardsUrl = self.pathUtil.keyman19KeyboardsDirectory else {
return false
}
return self.directoryExistsAtPath(directoryUrl: keyboardsUrl)
}
/**
* returns true if a directory exists at the specified URL
*/
func directoryExistsAtPath(directoryUrl: URL) -> Bool {
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: directoryUrl.path, isDirectory: &isDirectory)
return exists && isDirectory.boolValue
}
/**
* read keyboard packages at Keyman 19 location, inside Group Containers directory
*/
func readKeymanPackagesForKeyman19() -> [PackageSource] {
guard let keyboardsUrl = self.pathUtil.keyman19KeyboardsDirectory else {
return []
}
return readKeyboardPackageSource(keyboardDirectoryUrl: keyboardsUrl)
}
/**
* loop through all the sub-directories in the keyboards directory and try to read them as packages
*/
func readKeyboardPackageSource(keyboardDirectoryUrl: URL) -> [PackageSource] {
var packages: [PackageSource] = []
do {
// Get the URLs for all items in the directory that are not hidden
let directoryContents = try FileManager.default.contentsOfDirectory(
at: keyboardDirectoryUrl,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
for itemUrl in directoryContents {
// if the item is a directory, then attempt to read it as a keyboard package
if (itemUrl.hasDirectoryPath) {
if let packageSource = readKeyboardPackageFromDirectory(keyboardDirectoryUrl: itemUrl) {
packages.append(packageSource)
}
}
}
} catch {
print("Failed to read directory: \(error.localizedDescription)")
}
print("\(packages.count) packages read")
return packages
}
/**
* check the specified directory for kmp.json file and read it if it exists
*/
func readKeyboardPackageFromDirectory(keyboardDirectoryUrl: URL) -> PackageSource? {
var packageSource: PackageSource?
let lastPathComponent = keyboardDirectoryUrl.lastPathComponent
let kmpFileUrl = keyboardDirectoryUrl.appendingPathComponent(packageFileName)
if FileManager.default.fileExists(atPath: kmpFileUrl.path) {
if let source = readKeyboardPackage(kmpFileUrl) {
packageSource = source
// save the keyboard directory and path of the kmp.json for this keyboard
packageSource?.directoryUrl = keyboardDirectoryUrl
packageSource?.jsonFileUrl = kmpFileUrl
}
} else {
print(" ** \(lastPathComponent) DOES NOT contain the file 'kmp.json')")
}
return packageSource
}
/**
* read the kmp.json file at the specified URL
*/
func readKeyboardPackage(_ kmpFileUrl: URL) -> PackageSource? {
var packageSource: PackageSource?
do {
let jsonData = try Data(contentsOf: kmpFileUrl, options: .mappedIfSafe)
let source: PackageSource = try! JSONDecoder().decode(PackageSource.self, from: jsonData)
print("package name: \(source.packageName)")
packageSource = source
} catch {
// print("package name: \(source.packageName)")
}
return packageSource
}
}