Move file copying code to Storage

This commit is contained in:
Gabriel Wong 2017-11-27 14:11:34 +07:00
parent 03b95f7fed
commit cf7ef817e7
3 changed files with 148 additions and 226 deletions

View file

@ -51,4 +51,6 @@ public struct Constants {
font: defaultFont,
oskFont: nil,
isCustom: false)
static let kmwFileName = "keyboard.html"
}

View file

@ -28,13 +28,9 @@ public enum KeyboardState {
// Strings
private let keyboardChangeHelpText = "Tap here to change keyboard"
// URLs and Filenames
// URLs
private let apiBaseURL = "https://r.keymanweb.com/api/4.0/"
private let apiRemoteURL = "https://r.keymanweb.com/api/2.0/remote?url="
private let kmwFileName = "keyboard"
private let kmwFileExtension = "html"
private let kmwFullFileName = "\(kmwFileName).\(kmwFileExtension)"
private let iOSCodeFileName = "keymanios.js"
private let keymanHostName = "r.keymanweb.com"
// UI In-App Keyboard Constants
@ -191,7 +187,14 @@ UIGestureRecognizerDelegate {
if !isSystemKeyboard {
copyUserDefaultsToSharedContainer()
copyKeymanFilesToSharedContainer()
if let shared = Storage.shared,
let nonShared = Storage.nonShared {
do {
try nonShared.copyFiles(to: shared)
} catch {
kmLog("Failed to copy files to shared container: \(error)", checkDebugPrinting: false)
}
}
let userData = Storage.active.userDefaults
let isKPDisplayed = userData.bool(forKey: Key.keyboardPickerDisplayed)
if isKPDisplayed {
@ -201,7 +204,11 @@ UIGestureRecognizerDelegate {
isKeymanHelpOn = false
}
copyWebFilesToLibrary()
do {
try Storage.active.copyKMWFiles(from: keymanBundle)
} catch {
kmLog("Failed to copy KMW files from bundle: \(error)", checkDebugPrinting: false)
}
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow),
name: .UIKeyboardWillShow, object: nil)
@ -318,7 +325,8 @@ UIGestureRecognizerDelegate {
/// - font: Custom font for text views as a JSON String (see keyboardsDictionary)
/// - oskFont: Font for the on-screen keyboard
public func addKeyboard(_ keyboard: InstallableKeyboard) {
if !keyboardFileExists(withID: keyboard.id, version: keyboard.version) {
let keyboardPath = Storage.active.keyboardURL(for: keyboard).path
if !FileManager.default.fileExists(atPath: keyboardPath) {
kmLog("Could not add keyboard with ID: \(keyboard.id) because the keyboard file does not exist",
checkDebugPrinting: false)
return
@ -551,13 +559,13 @@ UIGestureRecognizerDelegate {
downloadQueue!.userInfo = commonUserData
var request = HTTPDownloadRequest(url: keyboardURL, userInfo: commonUserData)
request.destinationFile = keyboardPath(forID: keyboardID, keyboardVersion: keyboard.version)?.path
request.destinationFile = Storage.active.keyboardURL(for: keyboard).path
request.tag = 0
downloadQueue!.addRequest(request)
for (i, url) in fontURLs.enumerated() {
request = HTTPDownloadRequest(url: url, userInfo: commonUserData)
request.destinationFile = fontPath(forFilename: url.lastPathComponent)?.path
request.destinationFile = Storage.active.fontURL(forFilename: url.lastPathComponent).path
request.tag = i + 1
downloadQueue!.addRequest(request)
}
@ -628,7 +636,6 @@ UIGestureRecognizerDelegate {
return
}
let keyboardLocalPath = self.keyboardPath(forID: keyboard.id, keyboardVersion: keyboard.version)!
let isUpdate = latestKeyboardFileVersion(withID: keyboard.id) != nil
downloadQueue = HTTPDownloader.init(self)
@ -639,14 +646,13 @@ UIGestureRecognizerDelegate {
downloadQueue!.userInfo = commonUserData
var request = HTTPDownloadRequest(url: keyboardURL, userInfo: commonUserData)
request.destinationFile = keyboardLocalPath.path
request.destinationFile = Storage.active.keyboardURL(forID: keyboard.id, version: keyboard.version).path
request.tag = 0
downloadQueue!.addRequest(request)
for (i, url) in fontURLs.enumerated() {
let fontPath = self.fontPath(forFilename: url.lastPathComponent)!
request = HTTPDownloadRequest(url: url, userInfo: commonUserData)
request.destinationFile = fontPath.path
request.destinationFile = Storage.active.fontURL(forFilename: url.lastPathComponent).path
request.tag = i + 1
downloadQueue!.addRequest(request)
}
@ -807,9 +813,7 @@ UIGestureRecognizerDelegate {
if !isUpdate {
// Clean up keyboard file if anything fails
// TODO: Also clean up remaining fonts
if let kbPath = keyboardPath(forID: keyboard.id, keyboardVersion: keyboard.version) {
try? FileManager.default.removeItem(at: kbPath)
}
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboard))
}
downloadFailed(forKeyboards: keyboards, error: error)
}
@ -860,9 +864,7 @@ UIGestureRecognizerDelegate {
if !isUpdate {
// Clean up keyboard file if anything fails
// TODO: Also clean up remaining fonts
if let kbPath = keyboardPath(forID: keyboard.id, keyboardVersion: keyboard.version) {
try? FileManager.default.removeItem(at: kbPath)
}
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboard))
}
downloadFailed(forKeyboards: keyboards, error: error as NSError)
case .downloadCachedData:
@ -889,42 +891,24 @@ UIGestureRecognizerDelegate {
// MARK: - Loading custom keyboards
private func preloadFile(srcUrl: URL, dstDir dirUrl: URL, shouldOverwrite: Bool) {
let dstUrl = dirUrl.appendingPathComponent(srcUrl.lastPathComponent)
do {
if !FileManager.default.fileExists(atPath: dstUrl.path) {
try FileManager.default.copyItem(at: srcUrl, to: dstUrl)
} else if shouldOverwrite {
try FileManager.default.removeItem(at: dstUrl)
try FileManager.default.copyItem(at: srcUrl, to: dstUrl)
} else {
kmLog("File already exists at \(dstUrl) and not overwriting", checkDebugPrinting: true)
return
}
addSkipBackupAttribute(to: dstUrl)
} catch {
kmLog("Error copying file: \(error)", checkDebugPrinting: false)
}
}
/// Preloads a .js file for a language so that the keyboard is available without downloading.
/// - Precondition:
/// - The .js filename must remain the same as when obtained from Keyman.
/// - The .js file must be bundled in your application.
public func preloadLanguageFile(atPath languagePath: String, shouldOverwrite: Bool) {
preloadFile(srcUrl: URL.init(fileURLWithPath: languagePath),
dstDir: Storage.active.languageDir,
shouldOverwrite: shouldOverwrite)
public func preloadKeyboardFile(at url: URL, shouldOverwrite: Bool) throws {
try Storage.copyAndExcludeFromBackup(at: url,
to: Storage.active.languageDir.appendingPathComponent(url.lastPathComponent),
shouldOverwrite: shouldOverwrite)
}
/// Preloads a .ttf or .otf file to be available without downloading.
/// - Precondition:
/// - The font file must be bundled in your application.
/// - SeeAlso: `registerCustomFonts()`
public func preloadFontFile(atPath fontPath: String, shouldOverwrite: Bool) {
preloadFile(srcUrl: URL.init(fileURLWithPath: fontPath),
dstDir: Storage.active.fontDir,
shouldOverwrite: shouldOverwrite)
public func preloadFontFile(at url: URL, shouldOverwrite: Bool) throws {
try Storage.copyAndExcludeFromBackup(at: url,
to: Storage.active.fontDir.appendingPathComponent(url.lastPathComponent),
shouldOverwrite: shouldOverwrite)
}
/// Registers all new fonts found in the font path. Call this after you have preloaded all your font files
@ -1035,65 +1019,6 @@ UIGestureRecognizerDelegate {
}
// MARK: - File system and UserData management
// Local file storage
private func copyWebFilesToLibrary() {
do {
try copyFromBundle(resourceName: kmwFileName,
resourceExtension: kmwFileExtension,
dstDir: Storage.active.baseDir)
try copyFromBundle(resourceName: iOSCodeFileName,
resourceExtension: nil,
dstDir: Storage.active.baseDir)
try copyFromBundle(resourceName: "\(Constants.defaultKeyboard.id)-\(Constants.defaultKeyboard.version)",
resourceExtension: "js",
dstDir: Storage.active.languageDir)
try copyFromBundle(resourceName: "DejaVuSans",
resourceExtension: "ttf",
dstDir: Storage.active.fontDir)
try copyFromBundle(resourceName: "kmwosk",
resourceExtension: "css",
dstDir: Storage.active.baseDir)
try copyFromBundle(resourceName: "keymanweb-osk",
resourceExtension: "ttf",
dstDir: Storage.active.baseDir)
} catch {
kmLog("copyWebFilesToLibrary: \(error)", checkDebugPrinting: false)
}
}
private func copyFromBundle(resourceName: String, resourceExtension: String?, dstDir: URL?) throws {
let filenameForLog = "\(resourceName)\(resourceExtension.map { ".\($0)" } ?? "")"
guard let srcUrl = keymanBundle.url(forResource: resourceName, withExtension: resourceExtension) else {
let message = "Could not locate \(filenameForLog) in the Keyman bundle for copying."
throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
}
guard let dstDir = dstDir else {
let message = "Destination directory for \(filenameForLog) is nil"
throw NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
}
let dstUrl = dstDir.appendingPathComponent(srcUrl.lastPathComponent)
// FIXME: FileManager exceptions are swallowed.
copyAndExcludeFromBackup(at: srcUrl, to: dstUrl)
}
private func compareFileModDates(_ a: String, _ b: String) -> ComparisonResult? {
guard let aAttrs = try? FileManager.default.attributesOfItem(atPath: a),
let bAttrs = try? FileManager.default.attributesOfItem(atPath: b),
let aModDate = aAttrs[.modificationDate] as? Date,
let bModDate = bAttrs[.modificationDate] as? Date else {
return nil
}
if aModDate > bModDate {
return .orderedDescending
}
if aModDate < bModDate {
return .orderedAscending
}
return .orderedSame
}
private func copyUserDefaultsToSharedContainer() {
guard let sharedUserDefaults = Storage.shared?.userDefaults,
let defaultUserDefaults = Storage.nonShared?.userDefaults
@ -1125,117 +1050,6 @@ UIGestureRecognizerDelegate {
defaultUserDefaults.synchronize()
}
private func addSkipBackupAttribute(to url: URL) -> Bool {
var url = url
assert(FileManager.default.fileExists(atPath: url.path))
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
do {
// Writes values to the backing store. It is not only mutating the URL in memory.
try url.setResourceValues(resourceValues)
return true
} catch {
kmLog("Error excluding \(url) from backup \(error)", checkDebugPrinting: false)
return false
}
}
private func copyAndExcludeFromBackup(at src: URL, to dst: URL) -> Bool {
let fm = FileManager.default
var isDirectory: ObjCBool = false
let fileExists = fm.fileExists(atPath: src.path, isDirectory: &isDirectory)
if !fileExists || isDirectory.boolValue {
return false
}
// copy if destination does not exist or replace if source is newer
do {
if !fm.fileExists(atPath: dst.path) {
try fm.copyItem(at: src, to: dst)
} else if compareFileModDates(src.path, dst.path) == .orderedDescending {
try fm.removeItem(at: dst)
try fm.copyItem(at: src, to: dst)
} else {
return false
}
} catch {
kmLog("copyAndExcludeFromBackup: \(error)", checkDebugPrinting: false)
return false
}
addSkipBackupAttribute(to: dst)
return true
}
private func copyDirectoryContents(at srcDir: URL?, to dstDir: URL?) throws {
guard let srcDir = srcDir,
let dstDir = dstDir else {
return
}
let srcContents = try FileManager.default.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: [])
for srcFile in srcContents {
copyAndExcludeFromBackup(at: srcFile, to: dstDir.appendingPathComponent(srcFile.lastPathComponent))
}
}
private func copyKeymanFilesToSharedContainer() -> Bool {
guard let nonShared = Storage.nonShared,
let shared = Storage.shared
else {
return false
}
do {
try copyDirectoryContents(at: nonShared.baseDir, to: shared.baseDir)
try copyDirectoryContents(at: nonShared.languageDir, to: shared.languageDir)
try copyDirectoryContents(at: nonShared.fontDir, to: shared.fontDir)
return true
} catch {
kmLog("copyKeymanFilesToSharedContainer(): \(error)", checkDebugPrinting: false)
return false
}
}
private func copyKeymanFilesFromSharedContainer() -> Bool {
guard let nonShared = Storage.nonShared,
let shared = Storage.shared
else {
return false
}
do {
try copyDirectoryContents(at: shared.baseDir, to: nonShared.baseDir)
try copyDirectoryContents(at: shared.languageDir, to: nonShared.languageDir)
try copyDirectoryContents(at: shared.fontDir, to: nonShared.fontDir)
} catch {
kmLog("copyKeymanFilesFromSharedContainer(): \(error)", checkDebugPrinting: false)
return false
}
registerCustomFonts()
return true
}
// FIXME: The check for empty filename, etc was removed. Check whether that needs to be added back.
private func keyboardPath(forID keyboardID: String, keyboardVersion: String?) -> URL? {
var keyboardVersion = keyboardVersion
if keyboardVersion == nil {
keyboardVersion = latestKeyboardFileVersion(withID: keyboardID)
}
guard let version = keyboardVersion else {
return nil
}
return Storage.active.languageDir.appendingPathComponent("\(keyboardID)-\(version).js")
}
func fontPath(forFilename filename: String) -> URL? {
return Storage.active.fontDir.appendingPathComponent(filename)
}
func keyboardFileExists(withID keyboardID: String, version: String) -> Bool {
let path = Storage.active.languageDir.appendingPathComponent("\(keyboardID)-\(version).js").path
return FileManager.default.fileExists(atPath: path)
}
func latestKeyboardFileVersion(withID keyboardID: String) -> String? {
guard let dirContents = try? FileManager.default.contentsOfDirectory(atPath: Storage.active.languageDir.path) else {
return nil
@ -1319,7 +1133,15 @@ UIGestureRecognizerDelegate {
func synchronizeSWKeyboard() {
copyUserDefaultsFromSharedContainer()
copyKeymanFilesFromSharedContainer()
if let shared = Storage.shared,
let nonShared = Storage.nonShared {
do {
try shared.copyFiles(to: nonShared)
registerCustomFonts()
} catch {
kmLog("Failed to copy from shared container: \(error)", checkDebugPrinting: false)
}
}
}
// MARK: - View management
@ -1488,12 +1310,11 @@ UIGestureRecognizerDelegate {
}
private func reloadKeyboard(in view: WKWebView) {
let codeURL = Storage.active.baseDir.appendingPathComponent(kmwFullFileName)
if #available(iOS 9.0, *) {
view.loadFileURL(codeURL, allowingReadAccessTo: codeURL.deletingLastPathComponent())
view.loadFileURL(Storage.active.kmwURL, allowingReadAccessTo: Storage.active.baseDir)
} else {
// WKWebView in iOS < 9 is missing loadFileURL().
view.load(URLRequest(url: codeURL, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 60.0))
view.load(URLRequest(url: Storage.active.kmwURL, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 60.0))
}
}
@ -1698,7 +1519,7 @@ UIGestureRecognizerDelegate {
guard let url = webView.url else {
return
}
guard url.lastPathComponent == kmwFullFileName && (url.fragment?.isEmpty ?? true) else {
guard url.lastPathComponent == Constants.kmwFileName && (url.fragment?.isEmpty ?? true) else {
return
}

View file

@ -47,11 +47,13 @@ extension Storage {
}()
}
public class Storage {
public let baseDir: URL
public let languageDir: URL
public let fontDir: URL
public let userDefaults: UserDefaults
class Storage {
let baseDir: URL
let languageDir: URL
let fontDir: URL
let userDefaults: UserDefaults
let kmwURL: URL
private init?(baseURL: URL, userDefaults: UserDefaults) {
guard
@ -65,6 +67,7 @@ public class Storage {
self.languageDir = languageDir
self.fontDir = fontDir
self.userDefaults = userDefaults
kmwURL = baseDir.appendingPathComponent(Constants.kmwFileName)
}
private static func createSubdirectory(baseDir: URL?, name: String) -> URL? {
@ -82,4 +85,100 @@ public class Storage {
return nil
}
}
func keyboardURL(for keyboard: InstallableKeyboard) -> URL {
return keyboardURL(forID: keyboard.id, version: keyboard.version)
}
func keyboardURL(forID keyboardID: String, version: String) -> URL {
return languageDir.appendingPathComponent("\(keyboardID)-\(version).js")
}
func fontURL(forFilename filename: String) -> URL {
return fontDir.appendingPathComponent(filename)
}
}
// MARK: - Copying
extension Storage {
func copyKMWFiles(from bundle: Bundle) throws {
try Storage.copy(from: bundle,
resourceName: Constants.kmwFileName,
dstDir: Storage.active.baseDir)
try Storage.copy(from: bundle,
resourceName: "keymanios.js",
dstDir: Storage.active.baseDir)
try Storage.copy(from: bundle,
resourceName: "\(Constants.defaultKeyboard.id)-\(Constants.defaultKeyboard.version).js",
dstDir: Storage.active.languageDir)
try Storage.copy(from: bundle,
resourceName: "DejaVuSans.ttf",
dstDir: Storage.active.fontDir)
try Storage.copy(from: bundle,
resourceName: "kmwosk.css",
dstDir: Storage.active.baseDir)
try Storage.copy(from: bundle,
resourceName: "keymanweb-osk.ttf",
dstDir: Storage.active.baseDir)
}
func copyFiles(to dst: Storage) throws {
try Storage.copyDirectoryContents(at: baseDir, to: dst.baseDir)
try Storage.copyDirectoryContents(at: languageDir, to: dst.languageDir)
try Storage.copyDirectoryContents(at: fontDir, to: dst.fontDir)
}
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)
}
}