diff --git a/internal/bridge/mocks.go b/internal/bridge/mocks.go index ea0ddbfa..94efb0e8 100644 --- a/internal/bridge/mocks.go +++ b/internal/bridge/mocks.go @@ -63,7 +63,7 @@ func (j *TestCookieJar) Cookies(u *url.URL) []*http.Cookie { } type TestLocationsProvider struct { - config, cache string + config, data, cache string } func NewTestLocationsProvider(dir string) *TestLocationsProvider { @@ -72,6 +72,11 @@ func NewTestLocationsProvider(dir string) *TestLocationsProvider { panic(err) } + data, err := os.MkdirTemp(dir, "data") + if err != nil { + panic(err) + } + cache, err := os.MkdirTemp(dir, "cache") if err != nil { panic(err) @@ -79,6 +84,7 @@ func NewTestLocationsProvider(dir string) *TestLocationsProvider { return &TestLocationsProvider{ config: config, + data: data, cache: cache, } } @@ -87,6 +93,10 @@ func (provider *TestLocationsProvider) UserConfig() string { return provider.config } +func (provider *TestLocationsProvider) UserData() string { + return provider.data +} + func (provider *TestLocationsProvider) UserCache() string { return provider.cache } diff --git a/internal/locations/locations.go b/internal/locations/locations.go index b2c7f315..8a495460 100644 --- a/internal/locations/locations.go +++ b/internal/locations/locations.go @@ -36,16 +36,26 @@ import ( // - updates: ~/.config/protonmail//updates // - lockfile: ~/.cache/protonmail//.lock . type Locations struct { - userConfig, userCache string - configName string - configGuiName string + // userConfig is the path to the user config directory, for storing persistent config data. + userConfig string + + // userData is the path to the user data directory, for storing persistent data. + userData string + + // userCache is the path to the user cache directory, for storing non-essential data. + userCache string + + configName string + configGuiName string } // New returns a new locations object. func New(provider Provider, configName string) *Locations { return &Locations{ - userConfig: provider.UserConfig(), - userCache: provider.UserCache(), + userConfig: provider.UserConfig(), + userData: provider.UserData(), + userCache: provider.UserCache(), + configName: configName, configGuiName: configName + "-gui", } @@ -178,7 +188,7 @@ func (l *Locations) GetOldUpdatesPath() string { } func (l *Locations) getGluonPath() string { - return filepath.Join(l.userCache, "gluon") + return filepath.Join(l.userData, "gluon") } func (l *Locations) getGUICertPath() string { @@ -190,7 +200,7 @@ func (l *Locations) getSettingsPath() string { } func (l *Locations) getLogsPath() string { - return filepath.Join(l.userCache, "logs") + return filepath.Join(l.userData, "logs") } func (l *Locations) getGoIMAPCachePath() string { @@ -215,6 +225,7 @@ func (l *Locations) getUpdatesPath() string { func (l *Locations) Clear() error { return files.Remove( l.userConfig, + l.userData, l.userCache, ).Except( l.GetGuiLockFile(), @@ -232,7 +243,10 @@ func (l *Locations) ClearUpdates() error { // Clean removes any unexpected files from the app cache folder // while leaving files in the standard locations untouched. func (l *Locations) Clean() error { - return files.Remove(l.userCache).Except( + return files.Remove( + l.userCache, + l.userData, + ).Except( l.GetGuiLockFile(), l.getLogsPath(), l.getUpdatesPath(), diff --git a/internal/locations/locations_test.go b/internal/locations/locations_test.go index 096513dd..fabd04e7 100644 --- a/internal/locations/locations_test.go +++ b/internal/locations/locations_test.go @@ -27,13 +27,17 @@ import ( ) type fakeAppDirs struct { - configDir, cacheDir string + configDir, dataDir, cacheDir string } func (dirs *fakeAppDirs) UserConfig() string { return dirs.configDir } +func (dirs *fakeAppDirs) UserData() string { + return dirs.dataDir +} + func (dirs *fakeAppDirs) UserCache() string { return dirs.cacheDir } @@ -134,6 +138,7 @@ func TestRemoveOldGoIMAPCacheFolders(t *testing.T) { func newFakeAppDirs(t *testing.T) *fakeAppDirs { return &fakeAppDirs{ configDir: t.TempDir(), + dataDir: t.TempDir(), cacheDir: t.TempDir(), } } diff --git a/internal/locations/provider.go b/internal/locations/provider.go index e7083563..83d23594 100644 --- a/internal/locations/provider.go +++ b/internal/locations/provider.go @@ -18,19 +18,22 @@ package locations import ( + "errors" "os" "path/filepath" + "runtime" ) // Provider provides standard locations. type Provider interface { UserConfig() string + UserData() string UserCache() string } // DefaultProvider is a locations provider using the system-default storage locations. type DefaultProvider struct { - config, cache string + config, data, cache string } func NewDefaultProvider(name string) (*DefaultProvider, error) { @@ -39,6 +42,11 @@ func NewDefaultProvider(name string) (*DefaultProvider, error) { return nil, err } + data, err := userDataDir() + if err != nil { + return nil, err + } + cache, err := os.UserCacheDir() if err != nil { return nil, err @@ -46,14 +54,44 @@ func NewDefaultProvider(name string) (*DefaultProvider, error) { return &DefaultProvider{ config: filepath.Join(config, name), + data: filepath.Join(data, name), cache: filepath.Join(cache, name), }, nil } +// UserConfig returns a directory that can be used to store user-specific configuration. +// $XDG_CONFIG_HOME/protonmail is used on Unix systems; similar on others. func (p *DefaultProvider) UserConfig() string { return p.config } +// UserData returns a directory that can be used to store user-specific data. +// $XDG_DATA_HOME/protonmail is used on Unix systems; similar on others. +func (p *DefaultProvider) UserData() string { + return p.data +} + +// UserCache returns a directory that can be used to store user-specific non-essential data. +// $XDG_CACHE_HOME/protonmail is used on Unix systems; similar on others. func (p *DefaultProvider) UserCache() string { return p.cache } + +// userDataDir returns a directory that can be used to store user-specific data. +// This is necessary because os.UserDataDir() is not implemented by the Go standard library, sadly. +// On non-linux systems, it is the same as os.UserConfigDir(). +func userDataDir() (string, error) { + if runtime.GOOS != "linux" { + return os.UserConfigDir() + } + + if dir := os.Getenv("XDG_DATA_HOME"); dir != "" { + return dir, nil + } + + if dir := os.Getenv("HOME"); dir != "" { + return filepath.Join(dir, ".local", "share"), nil + } + + return "", errors.New("neither $XDG_DATA_HOME nor $HOME are defined") +}