From d8711cf7f974bb32becfda08868b9f312f45dd96 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Fri, 9 Apr 2021 20:36:25 +0200 Subject: [PATCH] config: create config file in windows appdata directory by default (#5226) Use %AppData% as primary default for configuration file on Windows, which is more in line with Windows standards, while existing default of using home directory is more Unix standards - though that made rclone more consistent accross different OS. Fixes #4667 --- docs/content/docs.md | 49 ++++++++-- fs/config/config.go | 222 ++++++++++++++++++++++++++++++------------- 2 files changed, 195 insertions(+), 76 deletions(-) diff --git a/docs/content/docs.md b/docs/content/docs.md index f61462c87..decf9f911 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -640,22 +640,51 @@ See `--copy-dest` and `--backup-dir`. ### --config=CONFIG_FILE ### -Specify the location of the rclone configuration file. +Specify the location of the rclone configuration file, to override +the default. E.g. `rclone config --config="rclone.conf"`. -Normally the config file is in your home directory as a file called -`.config/rclone/rclone.conf` (or `.rclone.conf` if created with an -older version). If `$XDG_CONFIG_HOME` is set it will be at -`$XDG_CONFIG_HOME/rclone/rclone.conf`. +The exact default is a bit complex to describe, due to changes +introduced through different versions of rclone while preserving +backwards compatibility, but in most cases it is as simple as: -If there is a file `rclone.conf` in the same directory as the rclone -executable it will be preferred. This file must be created manually -for Rclone to use it, it will never be created automatically. + - `%APPDATA%/rclone/rclone.conf` on Windows + - `~/.config/rclone/rclone.conf` on other + +The complete logic is as follows: Rclone will look for an existing +configuration file in any of the following locations, in priority order: + + 1. `rclone.conf` (in program directory, where rclone executable is) + 2. `%APPDATA%/rclone/rclone.conf` (only on Windows) + 3. `$XDG_CONFIG_HOME/rclone/rclone.conf` (on all systems, including Windows) + 4. `~/.config/rclone/rclone.conf` (see below for explanation of ~ symbol) + 5. `~/.rclone.conf` + +If no existing configuration file is found, then a new one will be created +in the following location: + +- On Windows: Location 2 listed above, except in the unlikely event + that `APPDATA` is not defined, then location 4 is used instead. +- On Unix: Location 3 if `XDG_CONFIG_HOME` is defined, else location 4. +- Fallback to location 5 (on all OS), when the rclone directory cannot be + created, but if also a home directory was not found then path + `.rclone.conf` relative to current working directory will be used as + a final resort. + +The `~` symbol in paths above represent the home directory of the current user +on any OS, and the value is defined as following: + + - On Windows: `%HOME%` if defined, else `%USERPROFILE%`, or else `%HOMEDRIVE%\%HOMEPATH%`. + - On Unix: `$HOME` if defined, else by looking up current user in OS-specific user database + (e.g. passwd file), or else use the result from shell command `cd && pwd`. If you run `rclone config file` you will see where the default location is for you. -Use this flag to override the config location, e.g. -`rclone config --config="rclone.conf"`. +The fact that an existing file `rclone.conf` in the same directory +as the rclone executable is always preferred, means that it is easy +to run in "portable" mode by downloading rclone executable to a +writable directory and then create an empty file `rclone.conf` in the +same directory. If the location is set to empty string `""` or the special value `/notfound`, or the os null device represented by value `NUL` on diff --git a/fs/config/config.go b/fs/config/config.go index 722cd7eb3..37ab42ad0 100644 --- a/fs/config/config.go +++ b/fs/config/config.go @@ -126,59 +126,127 @@ func init() { configPath = makeConfigPath() } +// Join directory with filename, and check if exists +func findFile(dir string, name string) string { + path := filepath.Join(dir, name) + if _, err := os.Stat(path); err != nil { + return "" + } + return path +} + +// Find current user's home directory +func findHomeDir() (string, error) { + path, err := homedir.Dir() + if err != nil { + fs.Debugf(nil, "Home directory lookup failed and cannot be used as configuration location: %v", err) + } else if path == "" { + // On Unix homedir return success but empty string for user with empty home configured in passwd file + fs.Debugf(nil, "Home directory not defined and cannot be used as configuration location") + } + return path, err +} + +// Find rclone executable directory and look for existing rclone.conf there +// (/rclone.conf) +func findLocalConfig() (configDir string, configFile string) { + if exePath, err := os.Executable(); err == nil { + configDir = filepath.Dir(exePath) + configFile = findFile(configDir, configFileName) + } + return +} + +// Get path to Windows AppData config subdirectory for rclone and look for existing rclone.conf there +// ($AppData/rclone/rclone.conf) +func findAppDataConfig() (configDir string, configFile string) { + if appDataDir := os.Getenv("APPDATA"); appDataDir != "" { + configDir = filepath.Join(appDataDir, "rclone") + configFile = findFile(configDir, configFileName) + } else { + fs.Debugf(nil, "Environment variable APPDATA is not defined and cannot be used as configuration location") + } + return +} + +// Get path to XDG config subdirectory for rclone and look for existing rclone.conf there +// (see XDG Base Directory specification: https://specifications.freedesktop.org/basedir-spec/latest/). +// ($XDG_CONFIG_HOME\rclone\rclone.conf) +func findXDGConfig() (configDir string, configFile string) { + if xdgConfigDir := os.Getenv("XDG_CONFIG_HOME"); xdgConfigDir != "" { + configDir = filepath.Join(xdgConfigDir, "rclone") + configFile = findFile(configDir, configFileName) + } + return +} + +// Get path to .config subdirectory for rclone and look for existing rclone.conf there +// (~/.config/rclone/rclone.conf) +func findDotConfigConfig(home string) (configDir string, configFile string) { + if home != "" { + configDir = filepath.Join(home, ".config", "rclone") + configFile = findFile(configDir, configFileName) + } + return +} + +// Look for existing .rclone.conf (legacy hidden filename) in root of user's home directory +// (~/.rclone.conf) +func findOldHomeConfig(home string) (configDir string, configFile string) { + if home != "" { + configDir = home + configFile = findFile(home, hiddenConfigFileName) + } + return +} + // Return the path to the configuration file func makeConfigPath() string { - // Use rclone.conf from rclone executable directory if already existing - exe, err := os.Executable() - if err == nil { - exedir := filepath.Dir(exe) - cfgpath := filepath.Join(exedir, configFileName) - _, err := os.Stat(cfgpath) - if err == nil { - return cfgpath + // Look for existing rclone.conf in prioritized list of known locations + // Also get configuration directory to use for new config file when no existing is found. + var ( + configFile string + configDir string + primaryConfigDir string + fallbackConfigDir string + ) + // /rclone.conf + if _, configFile = findLocalConfig(); configFile != "" { + return configFile + } + // Windows: $AppData/rclone/rclone.conf + // This is also the default location for new config when no existing is found + if runtime.GOOS == "windows" { + if primaryConfigDir, configFile = findAppDataConfig(); configFile != "" { + return configFile } } - - // Find user's home directory - homeDir, err := homedir.Dir() - - // Find user's configuration directory. - // Prefer XDG config path, with fallback to $HOME/.config. - // See XDG Base Directory specification - // https://specifications.freedesktop.org/basedir-spec/latest/), - xdgdir := os.Getenv("XDG_CONFIG_HOME") - var cfgdir string - if xdgdir != "" { - // User's configuration directory for rclone is $XDG_CONFIG_HOME/rclone - cfgdir = filepath.Join(xdgdir, "rclone") - } else if homeDir != "" { - // User's configuration directory for rclone is $HOME/.config/rclone - cfgdir = filepath.Join(homeDir, ".config", "rclone") + // $XDG_CONFIG_HOME/rclone/rclone.conf + // Also looking for this on Windows, for backwards compatibility reasons. + if configDir, configFile = findXDGConfig(); configFile != "" { + return configFile + } + if runtime.GOOS != "windows" { + // On Unix this is also the default location for new config when no existing is found + primaryConfigDir = configDir + } + // ~/.config/rclone/rclone.conf + // This is also the fallback location for new config + // (when $AppData on Windows and $XDG_CONFIG_HOME on Unix is not defined) + homeDir, homeDirErr := findHomeDir() + if fallbackConfigDir, configFile = findDotConfigConfig(homeDir); configFile != "" { + return configFile + } + // ~/.rclone.conf + if _, configFile = findOldHomeConfig(homeDir); configFile != "" { + return configFile } - // Use rclone.conf from user's configuration directory if already existing - var cfgpath string - if cfgdir != "" { - cfgpath = filepath.Join(cfgdir, configFileName) - _, err := os.Stat(cfgpath) - if err == nil { - return cfgpath - } - } - - // Use .rclone.conf from user's home directory if already existing - var homeconf string - if homeDir != "" { - homeconf = filepath.Join(homeDir, hiddenConfigFileName) - _, err := os.Stat(homeconf) - if err == nil { - return homeconf - } - } - - // Check to see if user supplied a --config variable or environment - // variable. We can't use pflag for this because it isn't initialised - // yet so we search the command line manually. + // No existing config file found, prepare proper default for a new one. + // But first check if if user supplied a --config variable or environment + // variable, since then we skip actually trying to create the default + // and report any errors related to it (we can't use pflag for this because + // it isn't initialised yet so we search the command line manually). _, configSupplied := os.LookupEnv("RCLONE_CONFIG") if !configSupplied { for _, item := range os.Args { @@ -188,32 +256,54 @@ func makeConfigPath() string { } } } - - // If user's configuration directory was found, then try to create it - // and assume rclone.conf can be written there. If user supplied config - // then skip creating the directory since it will not be used. - if cfgpath != "" { - // cfgpath != "" implies cfgdir != "" + // If we found a configuration directory to be used for new config during search + // above, then create it to be ready for rclone.conf file to be written into it + // later, and also as a test of permissions to use fallback if not even able to + // create the directory. + if primaryConfigDir != "" { + configDir = primaryConfigDir + } else if fallbackConfigDir != "" { + configDir = fallbackConfigDir + } else { + configDir = "" + } + if configDir != "" { + configFile = filepath.Join(configDir, configFileName) if configSupplied { - return cfgpath + // User supplied custom config option, just return the default path + // as is without creating any directories, since it will not be used + // anyway and we don't want to unnecessarily create empty directory. + return configFile } - err := os.MkdirAll(cfgdir, os.ModePerm) - if err == nil { - return cfgpath + var mkdirErr error + if mkdirErr = os.MkdirAll(configDir, os.ModePerm); mkdirErr == nil { + return configFile + } + // Problem: Try a fallback location. If we did find a home directory then + // just assume file .rclone.conf (legacy hidden filename) can be written in + // its root (~/.rclone.conf). + if homeDir != "" { + fs.Debugf(nil, "Configuration directory could not be created and will not be used: %v", mkdirErr) + return filepath.Join(homeDir, hiddenConfigFileName) + } + if !configSupplied { + fs.Errorf(nil, "Couldn't find home directory nor create configuration directory: %v", mkdirErr) + } + } else if !configSupplied { + if homeDirErr != nil { + fs.Errorf(nil, "Couldn't find configuration directory nor home directory: %v", homeDirErr) + } else { + fs.Errorf(nil, "Couldn't find configuration directory nor home directory") } } - - // Assume .rclone.conf can be written to user's home directory. - if homeconf != "" { - return homeconf - } - - // Default to ./.rclone.conf (current working directory) if everything else fails. + // No known location that can be used: Did possibly find a configDir + // (XDG_CONFIG_HOME or APPDATA) which couldn't be created, but in any case + // did not find a home directory! + // Report it as an error, and return as last resort the path relative to current + // working directory, of .rclone.conf (legacy hidden filename). if !configSupplied { - fs.Errorf(nil, "Couldn't find home directory or read HOME or XDG_CONFIG_HOME environment variables.") fs.Errorf(nil, "Defaulting to storing config in current directory.") fs.Errorf(nil, "Use --config flag to workaround.") - fs.Errorf(nil, "Error was: %v", err) } return hiddenConfigFileName }