Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(mac): store data in Library directory instead of Documents #12106

Merged
merged 12 commits into from
Aug 30, 2024

Conversation

sgschantz
Copy link
Contributor

@sgschantz sgschantz commented Aug 6, 2024

Store all keyman keyboards in ~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards
instead of the old location ~/Documents/Keyman-Keyboards

The new location is more appropriate because it is the standard location designated by Apple.

Also, beginning with macOS Catalina in 2019, access to the /Documents directory by an application required the consent of the user. The new location will not require special permission.

This change involves

  1. moving all the keyboard data files to the new location and
  2. updating the settings stored in the UserDefaults for Keyman

The UserDefaults path settings need updating for the selected keyboard, the list of active keyboards and the list of keyboards with persisted options.

@keymanapp-test-bot skip

@sgschantz sgschantz added this to the A18S8 milestone Aug 6, 2024
@sgschantz sgschantz self-assigned this Aug 6, 2024
@keymanapp-test-bot
Copy link

keymanapp-test-bot bot commented Aug 6, 2024

User Test Results

Test specification and instructions

User tests are not required

Test Artifacts

@keymanapp-test-bot keymanapp-test-bot bot added has-user-test user-test-required User tests have not been completed labels Aug 7, 2024
@keymanapp-test-bot keymanapp-test-bot bot removed has-user-test user-test-required User tests have not been completed labels Aug 9, 2024
@sgschantz sgschantz marked this pull request as ready for review August 9, 2024 08:03
@sgschantz sgschantz requested a review from SabineSIL as a code owner August 9, 2024 08:03
@mcdurdin
Copy link
Member

This seems like it needs a bunch of user tests around install and upgrade scenarios.

@sgschantz
Copy link
Contributor Author

This seems like it needs a bunch of user tests around install and upgrade scenarios.

The user tests are going to be under #12144. (The first one is there.) That is the second phase of this change, and I wanted to avoid doing the user tests until both parts are in place because the data format is changing from the first to the second.

@darcywong00 darcywong00 modified the milestones: A18S8, A18S9 Aug 17, 2024
Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. I have some nits, mostly comments I think are out of sync with implementation in a few places, but one thing that looks like a bug

Comment on lines 19 to 20
@property (readonly) NSURL *documentsSubDirectory; // '~/Documents'
@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory; // '~/Library/Documents/Keyman-Keyboards'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between the two Documents paths?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentsSubDirectory is the user's Documents directory, identified using an API. The obsolete directory is the full path to the place we formerly stored data under the Documents directory.

Looking at the code, I can eliminate the property documentsSubDirectory as it is only used in finding the full obsolete data path. Would that help simplify this? It is a special location, so I thought it would be useful to save the reference, but we really never want to access it again after the migration.

I'll change the comments to further clarify.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After working on the comments a little. I think I need a better comment that explains the hierarchical relationship between these. We have three directories we should know of going forward and two obsolete ones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sounds good, thanks

Comment on lines 35 to 36
(Using the subsystem id, "com.keyman.app", is a poor choice because the API
createDirectoryAtPath sees the .app extension and creates an application file.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments in KMDataRepository.h:L16-18 says that we are using com.keyman.app. So is that a poor choice?

Copy link
Contributor Author

@sgschantz sgschantz Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll fix the comments which are incorrect. I prefer com.keyman.app as an app ID but since our app ID is already set to keyman.inputmethod.Keyman and com.keyman.app looks to the file manager like a filename of type .app, I plan to stick with keyman.inputmethod.Keyman as the name of our app's data directory.

Comment on lines 40 to 41
+ (KMDataRepository *)shared
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ (KMDataRepository *)shared
{
+ (KMDataRepository *)shared {


- (NSURL *)keymanDataDirectory {
if (_keymanDataDirectory == nil) {
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if applicationSupportSubDirectory returns nil? Here and other similar calls in various places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot think of anything intelligent to do other than log an error if the OS does not return it to me. This is the place that we are supposed to create our app subdirectory for our user data, so if it doesn't exist, then we don't have a fallback location that I am aware of. In my 'Application Support' directory, there are 91 subdirectories created by other software, so it would be a major failure if it weren't there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess so. But currently if it does go wrong we'll crash the app. And if is never going to go wrong, then why bother with the additional error logging? So it feels like we are half way.

}

/**
* Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the header it says the data directory is '~/Library/Application Support/com.keyman.app'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header was wrong. 😒

Comment on lines +1 to +9
/**
* Keyman is copyright (C) SIL International. MIT License.
*
* KMSettingsRepository.h
* Keyman
*
* Created by Shawn Schantz on 2024-07-29.
*
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit: these headers don't quite match the code style guide -- we don't normally include filename, create date, author. Also just a single star on the first line.

https://github.com/keymanapp/keyman/wiki/Keyman-Code-Style-Guide#File-header

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I thought I was following a pattern that I copied from the standards a while back. I thought we even had a discussion about the date format.

I think that author and date can be very helpful to understand the context of the code at a glance without jumping into git. We can talk separately about this, but Is there a strong reason to exclude those?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that author and date can be very helpful to understand the context of the code at a glance without jumping into git. We can talk separately about this, but Is there a strong reason to exclude those?

The primary reasons not to do it are:

  1. avoid multiple "sources of truth" -- the initial creator of the file may well not be the most relevant, nor the create date
  2. avoid busy work

I think we can be pretty confident that git will get it right. In most good code editors, we can see git blame line-by-line, which nicely eliminates the burden of seeing the git data. I would encourage going to git for this kind of metadata -- commit history is really helpful (probably more helpful than original author + date IMO).

The only real downside I can see is if you want to see the original commit: if the header changes over time, then the git blame will show most recent commit against it by default, not the original, and it takes a little more digging to get back to the first commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any of these things override the practical value of this tiny piece of metadata: who created this thing and when. It is written once in the life of the file, and only ever updated with maybe a second author if there is a major redesign within the same code.

In most cases, it will remain as one name and one date but can reveal a lot on its own without scanning the commit history. If it doesn't seem helpful, it can easily be ignored along with the copyright statement. In my experience, the initial author is heavily involved in the design of the code (often the sole person responsible), and a lot of the original design persists even after many edits and attempts to refactor away from the original design.

For the Mac code at least, it looks like the first commit was not the author at all but just the guy who first made use of git. So the real author would be anonymous if he hadn't included his name in the header.

In short, I don't see this as redundant information but something with long-term practical value and maintenance cost that is approximately zero.

The file name and project name does seem like redundant busywork in the header, so I agree that we should remove those. (I've changed the class name many times and forgotten to update the header.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I am not overly bothered either way. Personally I feel like it is busy work which replicates what we now have in git. Yes, back in the day we didn't use git but that's a slippery argument because then we end up putting all commit history back into source files because what if we change vcs in the future and cannot transfer commit history and well yuck.

See for example https://github.com/keymanapp/keyman/blob/d8986be12fe0fb2078b93fa4650e2a7fe06f16d1/windows/src/engine/keyman32/keyman32.cpp for what things looked like before we relied on git history. If you scan through the file you'll also see // I1234 comments which were how we tagged changes. It worked but it was increasingly noisy so it was really good to move that change history into vcs metadata.

tl;dr: If you want to keep it in, OK, and then please also update the code style wiki to say that we can optionally include the Created by <author> on yyyy-mm-dd line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I find that one extra line to be helpful. I've updated the wiki.

Comment on lines 54 to 55
- (BOOL)settingsExist
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The brace format throughout is not quite consistent. Our style guide would indicate

Suggested change
- (BOOL)settingsExist
{
- (BOOL)settingsExist {

https://github.com/keymanapp/keyman/wiki/Keyman-Code-Style-Guide#code-format

*/
- (BOOL)dataStoredInLibraryDirectory
{
return [[NSUserDefaults standardUserDefaults] integerForKey:kDataModelVersion] == kVersionStoreDataInLibraryDirectory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return [[NSUserDefaults standardUserDefaults] integerForKey:kDataModelVersion] == kVersionStoreDataInLibraryDirectory;
return [[NSUserDefaults standardUserDefaults] integerForKey:kDataModelVersion] >= kVersionStoreDataInLibraryDirectory;

This seems more forward-compatible -- unlikely that we'll move data out of that folder again but likely we may change format

* Determines whether the keyboard data needs to be moved from the old location in ~/Documents to the new location ~/Library...
* This is true if
* 1) the UserDefaults exist (indicating that this is not a new installation of Keyman) and
* 2) the value for KMStoreKeyboardsInLibraryKey is not set to true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is out of date

BOOL dataInLibrary = [self dataStoredInLibraryDirectory];
os_log([KMLogs dataLog], " data stored in Library: %{public}@", dataInLibrary ? @"YES" : @"NO" );

return !(keymanSettingsExist && dataInLibrary);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems wrong. If the keyman settings don't exist, we don't want to attempt to read from documents folder

Suggested change
return !(keymanSettingsExist && dataInLibrary);
return keymanSettingsExist && !dataInLibrary;

Copy link
Contributor Author

@sgschantz sgschantz Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I thought it was just a bad method name and was rewriting it, but we should not be attempting to move the data if we have no settings. I'll fix that.

Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, changes look good, formatting and comment nits only now!

Comment on lines 3 to 5
*
* KMDataRepository.h
* Keyman
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
* KMDataRepository.h
* Keyman

Comment on lines 16 to 18
@property (readonly) NSURL *keymanDataDirectory; // '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keymanKeyboardsDirectory;
// keymanKeyboardsDirectory = '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit only, but I was puzzled about what looked like commented code

Suggested change
@property (readonly) NSURL *keymanDataDirectory; // '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keymanKeyboardsDirectory;
// keymanKeyboardsDirectory = '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
// keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keymanDataDirectory;
// keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
@property (readonly) NSURL *keymanKeyboardsDirectory;

Comment on lines 3 to 5
*
* KMResourcesRepository.m
* Keyman
Copy link
Member

@mcdurdin mcdurdin Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
* KMResourcesRepository.m
* Keyman

Filename was wrong here too!

Comment on lines 42 to 47
/*
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.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/*
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.
*/
/**
* 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.)
*/


- (NSURL *)keymanDataDirectory {
if (_keymanDataDirectory == nil) {
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess so. But currently if it does go wrong we'll crash the app. And if is never going to go wrong, then why bother with the additional error logging? So it feels like we are half way.

}
return _obsoleteKeymanKeyboardsDirectory;
}
/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
/**

Comment on lines 3 to 5
*
* KMSettingsRepository.h
* Keyman
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*
* KMSettingsRepository.h
* Keyman

Case in point - this says KMSettingsRepository.h but it is in a .m 😁

* For versions before version 1, the keyboards were stored under the ~/Documents directory.
*/
- (BOOL)dataModelWithKeyboardsInLibrary {
// NSUserDefaults returns zero if the key does not exist
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NSUserDefaults returns zero if the key does not exist
// [NSUserDefaults integerForKey] returns zero if the key does not exist

Copy link
Contributor Author

@sgschantz sgschantz Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue #12294 to document the need for better error handling in case the data directory cannot be located or is unreadable. The current code does not gracefully handle a nil returned from the OS when trying to access ~/Library/Application Support

mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m Outdated Show resolved Hide resolved
@sgschantz sgschantz merged commit a283fd1 into master Aug 30, 2024
5 checks passed
@sgschantz sgschantz deleted the change/mac/2542-store-keyboards-elsewhere branch August 30, 2024 05:35
@keyman-server
Copy link
Collaborator

Changes in this pull request will be available for download in Keyman version 18.0.102-alpha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants