mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-20 23:37:41 +00:00
230 lines
10 KiB
Objective-C
230 lines
10 KiB
Objective-C
/*
|
|
* Keyman is copyright (C) SIL International. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2024-07-30.
|
|
*
|
|
* Singleton object which serves as an abstraction for the reading and writing of Keyman data.
|
|
* The 'data' currently consists of keyman keyboards installed by the user. All data is saved locally on disk using NSFileManager.
|
|
* This is in contrast with the lighter weight Settings which is stored using UserDefaults and handled by KMSettingsRepository.
|
|
*/
|
|
|
|
#import "KMDataRepository.h"
|
|
#import "KMLogs.h"
|
|
|
|
@interface KMDataRepository ()
|
|
@property (readonly) NSURL *applicationSupportSubDirectory;
|
|
@property (readonly) NSURL *documentsSubDirectory;
|
|
@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory;
|
|
@end
|
|
|
|
@implementation KMDataRepository
|
|
/**
|
|
* Two directory trees are represented by the following properties, one in active use
|
|
* and one that is obsolete.
|
|
* The actively used directories, begin with the parent
|
|
* applicationSupportSubDirectory: '~/Library/Application Support'
|
|
* keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
|
|
* keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
|
|
* The obsolete directories, begin with the parent
|
|
* documentsSubDirectory: '~/Documents'
|
|
* obsoleteKeymanKeyboardsDirectory: '~/Documents/Keyman-Keyboards'
|
|
*/
|
|
@synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory;
|
|
@synthesize keymanDataDirectory = _keymanDataDirectory;
|
|
@synthesize keymanKeyboardsDirectory = _keymanKeyboardsDirectory;
|
|
@synthesize documentsSubDirectory = _documentsSubDirectory;
|
|
@synthesize obsoleteKeymanKeyboardsDirectory = _obsoleteKeymanKeyboardsDirectory;
|
|
|
|
NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards";
|
|
/**
|
|
* The name of the subdirectory within '~/Library/Application Support'.
|
|
* We follow the convention of using the bundle identifier rather than our subsystem id.
|
|
* (Also, using the subsystem id, "com.keyman.app", is a poor choice because the API
|
|
* createDirectoryAtPath sees the .app extension and creates an application file.)
|
|
*/
|
|
NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
|
|
|
|
+ (KMDataRepository *)shared {
|
|
static KMDataRepository *shared = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
shared = [[KMDataRepository alloc] init];
|
|
});
|
|
return shared;
|
|
}
|
|
|
|
- (NSURL *)documentsSubDirectory {
|
|
if (_documentsSubDirectory == nil) {
|
|
NSError *directoryError = nil;
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSURL *documentsUrl = [fileManager URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError];
|
|
|
|
if (directoryError) {
|
|
os_log_error([KMLogs dataLog], "error getting Documents subdirectory: '%{public}@'", directoryError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Documents subdirectory: '%{public}@'", documentsUrl);
|
|
_documentsSubDirectory = documentsUrl;
|
|
}
|
|
}
|
|
return _documentsSubDirectory;
|
|
}
|
|
|
|
- (NSURL *)applicationSupportSubDirectory {
|
|
if (_applicationSupportSubDirectory == nil) {
|
|
NSError *directoryError = nil;
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSURL *applicationSupportUrl = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError];
|
|
|
|
if (directoryError) {
|
|
os_log_error([KMLogs dataLog], "error getting Application Support subdirectory: '%{public}@'", directoryError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Application Support subdirectory: '%{public}@'", applicationSupportUrl.path);
|
|
_applicationSupportSubDirectory = applicationSupportUrl;
|
|
}
|
|
}
|
|
return _applicationSupportSubDirectory;
|
|
}
|
|
|
|
- (NSURL *)keymanDataDirectory {
|
|
if (_keymanDataDirectory == nil) {
|
|
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
|
|
_keymanDataDirectory = keymanDataUrl;
|
|
}
|
|
return _keymanDataDirectory;
|
|
}
|
|
|
|
- (NSURL *)keymanKeyboardsDirectory {
|
|
if (_keymanKeyboardsDirectory == nil) {
|
|
NSURL *keyboardsUrl = [self.keymanDataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
|
|
_keymanKeyboardsDirectory = keyboardsUrl;
|
|
}
|
|
return _keymanKeyboardsDirectory;
|
|
}
|
|
|
|
/**
|
|
* Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman
|
|
*/
|
|
- (void)createDataDirectoryIfNecessary {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDir;
|
|
BOOL exists = [fileManager fileExistsAtPath:self.keymanDataDirectory.path isDirectory:&isDir];
|
|
|
|
if (!exists) {
|
|
NSError *createError = nil;
|
|
os_log_info([KMLogs dataLog], "createDataDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanDataDirectory.path);
|
|
[fileManager createDirectoryAtPath:self.keymanDataDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
|
|
if (createError) {
|
|
os_log_error([KMLogs dataLog], "error creating Keyman data directory: '%{public}@'", createError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keymanDataDirectory.path);
|
|
}
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keymanDataDirectory.path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates Keyman keyboard directory if it does not exist yet. This is the 'Keyman-Keyboards' directory.
|
|
* It should not be created until after migrating because its existence would block migrating data from the old location.
|
|
*/
|
|
- (void)createKeyboardsDirectoryIfNecessary {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDir;
|
|
BOOL exists = [fileManager fileExistsAtPath:self.keymanKeyboardsDirectory.path isDirectory:&isDir];
|
|
|
|
if (!exists) {
|
|
NSError *createError = nil;
|
|
os_log_info([KMLogs dataLog], "createKeyboardsDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanKeyboardsDirectory.path);
|
|
[fileManager createDirectoryAtPath:self.keymanKeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
|
|
if (createError) {
|
|
os_log_error([KMLogs dataLog], "error creating Keyman-Keyboards directory: '%{public}@'", createError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "created Keyman-Keyboards subdirectory: '%{public}@'", self.keymanKeyboardsDirectory.path);
|
|
}
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Keyman-Keyboards already exists: '%{public}@'", self.keymanKeyboardsDirectory.path);
|
|
}
|
|
}
|
|
|
|
- (NSURL *)obsoleteKeymanKeyboardsDirectory {
|
|
if (_obsoleteKeymanKeyboardsDirectory == nil) {
|
|
NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
|
|
_obsoleteKeymanKeyboardsDirectory = keymanUrl;
|
|
}
|
|
return _obsoleteKeymanKeyboardsDirectory;
|
|
}
|
|
|
|
/**
|
|
* Only called from migrateData.
|
|
* Causes user to be prompted for permission to access ~/Documents, but they should already have it.
|
|
* otherwise we would not be attempting to migrate.
|
|
*/
|
|
- (BOOL)keyboardsExistInObsoleteDirectory {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDir;
|
|
BOOL exists = ([fileManager fileExistsAtPath:self.obsoleteKeymanKeyboardsDirectory.path isDirectory:&isDir]);
|
|
return exists;
|
|
}
|
|
|
|
/**
|
|
* Migrate the keyboards data from the old location in '~/Documents' to the new location '~/Application Support/keyman.inputmethod.Keyman/'
|
|
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
|
|
* If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory.
|
|
* If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again.
|
|
*/
|
|
- (BOOL)migrateData {
|
|
BOOL didMoveData = NO;
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL dataExistsInOldLocation = [self keyboardsExistInObsoleteDirectory];
|
|
os_log_debug([KMLogs dataLog], "obsolete keyman keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO");
|
|
|
|
// only move data if there is something to move
|
|
if (dataExistsInOldLocation) {
|
|
NSError *moveError = nil;
|
|
didMoveData = [fileManager moveItemAtURL:self.obsoleteKeymanKeyboardsDirectory
|
|
toURL:self.keymanKeyboardsDirectory
|
|
error:&moveError];
|
|
if (moveError) {
|
|
os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keymanKeyboardsDirectory.path);
|
|
}
|
|
}
|
|
|
|
return didMoveData;
|
|
}
|
|
|
|
- (NSString*)buildFullPath:(NSString *)fromPartialPath {
|
|
NSString *fullPath = [self.keymanKeyboardsDirectory.path stringByAppendingString:fromPartialPath];
|
|
os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'",
|
|
fullPath, fromPartialPath);
|
|
return fullPath;
|
|
}
|
|
|
|
- (NSString *)trimToPartialPath:(NSString *)fromFullPath {
|
|
NSString *partialPath = fromFullPath;
|
|
if(fromFullPath != nil) {
|
|
NSRange range = [fromFullPath rangeOfString:kKeyboardsDirectoryName];
|
|
if (range.length > 0) {
|
|
partialPath = [fromFullPath substringFromIndex:range.location + range.length];
|
|
os_log_debug([KMLogs dataLog], "trimToPartialPath: fromFullPath: '%{public}@' to partialPath: '%{public}@'", fromFullPath, partialPath);
|
|
}
|
|
}
|
|
return partialPath;
|
|
}
|
|
|
|
- (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename {
|
|
NSMutableArray *pathComponents = [[NSMutableArray alloc] initWithCapacity:0];
|
|
[pathComponents addObject:@"/"];
|
|
[pathComponents addObject:keyboardSubdirectory];
|
|
[pathComponents addObject:kmxFilename];
|
|
NSString *keyboardPartialPath = [NSString pathWithComponents:pathComponents];
|
|
os_log_debug([KMLogs keyboardLog], "buildPartialPathFrom, keyboardSubdirectory: %{public}@, kmxFileName: %{public}@, keyboardPartialPath : %{public}@",
|
|
keyboardSubdirectory, kmxFilename, keyboardPartialPath);
|
|
return keyboardPartialPath;
|
|
}
|
|
|
|
|
|
@end
|