spiegel-keyman/ios/engine/KMEI/KeymanEngine/Classes/Storage.swift
2017-12-18 09:52:37 +07:00

216 lines
7.6 KiB
Swift

//
// Storage.swift
// KeymanEngine
//
// Created by Gabriel Wong on 2017-11-21.
// Copyright © 2017 SIL International. All rights reserved.
//
import Foundation
// MARK: - Static members
extension Storage {
/// Storage that is not shared with the other process (app or extension).
static let nonShared: Storage? = {
let paths = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)
if paths.isEmpty {
return nil
}
return Storage(baseURL: paths[0], userDefaults: UserDefaults.standard)
}()
/// Storage shared between the app and app extension.
/// - Important: `Manager.applicationGroupIdentifier` must be set before accessing this. Modifications to
/// `Manager.applicationGroupIdentifier` will not be reflected here.
static let shared: Storage? = {
guard let groupID = Manager.applicationGroupIdentifier else {
return nil
}
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID),
let userDefaults = UserDefaults(suiteName: groupID)
else {
return nil
}
return Storage(baseURL: url, userDefaults: userDefaults)
}()
/// Active storage to be used for app data. This is `shared` if possible, otherwise `nonShared`.
/// - Important: `Manager.applicationGroupIdentifier` must be set before accessing this. Modifications to
/// `Manager.applicationGroupIdentifier` will not be reflected here.
static let active: Storage! = {
if let shared = shared {
return shared
}
return nonShared
}()
}
class Storage {
let baseDir: URL
let keyboardDir: URL
let userDefaults: UserDefaults
let kmwURL: URL
let specialOSKFontURL: URL
private init?(baseURL: URL, userDefaults: UserDefaults) {
guard
let baseDir = Storage.createSubdirectory(baseDir: baseURL, name: "keyman"),
let keyboardDir = Storage.createSubdirectory(baseDir: baseDir, name: "keyboards")
else {
return nil
}
self.baseDir = baseDir
self.keyboardDir = keyboardDir
self.userDefaults = userDefaults
kmwURL = baseDir.appendingPathComponent(Resources.kmwFilename)
specialOSKFontURL = baseDir.appendingPathComponent(Resources.oskFontFilename)
}
private static func createSubdirectory(baseDir: URL, name: String) -> URL? {
let newDir = baseDir.appendingPathComponent(name)
do {
try FileManager.default.createDirectory(at: newDir,
withIntermediateDirectories: true,
attributes: nil)
return newDir
} catch {
log.error("Failed to create subdirectory at \(newDir)")
return nil
}
}
func keyboardDir(forID keyboardID: String) -> URL {
return keyboardDir.appendingPathComponent(keyboardID)
}
func keyboardURL(for keyboard: InstallableKeyboard) -> URL {
return keyboardURL(forID: keyboard.id, version: keyboard.version)
}
func keyboardURL(forID keyboardID: String, version: String) -> URL {
return keyboardDir(forID: keyboardID).appendingPathComponent("\(keyboardID)-\(version).js")
}
func fontURL(forKeyboardID keyboardID: String, filename: String) -> URL {
return keyboardDir(forID: keyboardID).appendingPathComponent(filename)
}
var keyboardDirs: [URL]? {
let contents: [URL]
do {
contents = try FileManager.default.contentsOfDirectory(at: keyboardDir,
includingPropertiesForKeys: [.isDirectoryKey])
} catch {
log.error("Failed to list contents at \(keyboardDir) with error \(error)")
return nil
}
return contents.filter { url in
do {
let values = try url.resourceValues(forKeys: [.isDirectoryKey])
return values.isDirectory ?? false
} catch {
log.error(error)
return false
}
}
}
}
// MARK: - Copying
extension Storage {
func copyKMWFiles(from bundle: Bundle) throws {
try Storage.copy(from: bundle,
resourceName: Resources.kmwFilename,
dstDir: baseDir)
try Storage.copy(from: bundle,
resourceName: "keymanios.js",
dstDir: baseDir)
try Storage.copy(from: bundle,
resourceName: "kmwosk.css",
dstDir: baseDir)
try Storage.copy(from: bundle,
resourceName: "keymanweb-osk.ttf",
dstDir: baseDir)
let defaultKeyboardDir = self.keyboardDir(forID: Defaults.keyboard.id)
try FileManager.default.createDirectory(at: defaultKeyboardDir, withIntermediateDirectories: true)
try Storage.copy(from: bundle,
resourceName: "\(Defaults.keyboard.id)-\(Defaults.keyboard.version).js",
dstDir: defaultKeyboardDir)
try Storage.copy(from: bundle,
resourceName: "DejaVuSans.ttf",
dstDir: defaultKeyboardDir)
}
func copyFiles(to dst: Storage) throws {
if FileManager.default.fileExists(atPath: dst.baseDir.path) {
log.info("Deleting \(dst.baseDir) for copy from \(baseDir) to \(dst.baseDir)")
try FileManager.default.removeItem(at: dst.baseDir)
}
try FileManager.default.copyItem(at: baseDir, to: dst.baseDir)
}
func copyUserDefaults(to dst: Storage, withKeys keys: [String], shouldOverwrite: Bool) {
for key in keys {
if shouldOverwrite || dst.userDefaults.object(forKey: key) == nil {
dst.userDefaults.set(userDefaults.object(forKey: key), forKey: key)
}
}
dst.userDefaults.synchronize()
}
private static func copy(from bundle: Bundle, resourceName: String, dstDir: URL) throws {
guard let srcURL = bundle.url(forResource: resourceName, withExtension: nil) else {
let message = "Could not locate \(resourceName) in the Keyman bundle"
throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
}
let dstURL = dstDir.appendingPathComponent(srcURL.lastPathComponent)
return try Storage.copyAndExcludeFromBackup(at: srcURL, to: dstURL)
}
private static func copyDirectoryContents(at srcDir: URL, to dstDir: URL) throws {
let srcContents = try FileManager.default.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: [])
for srcFile in srcContents {
try Storage.copyAndExcludeFromBackup(at: srcFile, to: dstDir.appendingPathComponent(srcFile.lastPathComponent))
}
}
private static func addSkipBackupAttribute(to url: URL) throws {
var url = url
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
// Writes values to the backing store. It is not only mutating the URL in memory.
try url.setResourceValues(resourceValues)
}
/// Copy and exclude from iCloud backup if `src` is newer than `dst`. Does nothing for a directory.
static func copyAndExcludeFromBackup(at src: URL,
to dst: URL,
shouldOverwrite: Bool = true) throws {
let fm = FileManager.default
var isDirectory: ObjCBool = false
let fileExists = fm.fileExists(atPath: src.path, isDirectory: &isDirectory)
if isDirectory.boolValue {
return
}
if !fileExists {
let message = "Nothing to copy at \(src)"
throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
}
if !fm.fileExists(atPath: dst.path) {
try fm.copyItem(at: src, to: dst)
} else if shouldOverwrite {
try fm.removeItem(at: dst)
try fm.copyItem(at: src, to: dst)
} else {
return
}
try Storage.addSkipBackupAttribute(to: dst)
}
}