From e25e71cdde9525d215eeb7dcf4053586c4ca6d3a Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Wed, 18 Mar 2020 20:58:11 +0100 Subject: [PATCH] cmd/syncthing, lib/locations: Separate data and config dirs (fixes #4924) (#6309) --- cmd/syncthing/main.go | 75 ++++++++++++++------ etc/linux-desktop/syncthing-start.desktop | 2 +- lib/locations/locations.go | 85 +++++++++++++++-------- 3 files changed, 110 insertions(+), 52 deletions(-) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index ed45c7d9e..06175d616 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -65,6 +65,12 @@ I.e. to prefix each log line with date and time, set -logflags=3 (1 + 2 from above). The value 0 is used to disable all of the above. The default is to show time only (2). +Logging always happens to the command line (stdout) and optionally to the +file at the path specified by -logfile=path. In addition to an path, the special +values "default" and "-" may be used. The former logs to DATADIR/syncthing.log +(see -data-dir), which is the default on Windows, and the latter only to stdout, +no file, which is the default anywhere else. + Development Settings -------------------- @@ -149,7 +155,9 @@ var ( type RuntimeOptions struct { syncthing.Options + homeDir string confDir string + dataDir string resetDatabase bool showVersion bool showPaths bool @@ -197,11 +205,13 @@ func defaultRuntimeOptions() RuntimeOptions { options.logFlags = logger.DebugFlags } - if runtime.GOOS != "windows" { - // On non-Windows, we explicitly default to "-" which means stdout. On - // Windows, the blank options.logFile will later be replaced with the - // default path, unless the user has manually specified "-" or - // something else. + // On non-Windows, we explicitly default to "-" which means stdout. On + // Windows, the "default" options.logFile will later be replaced with the + // default path, unless the user has manually specified "-" or + // something else. + if runtime.GOOS == "windows" { + options.logFile = "default" + } else { options.logFile = "-" } @@ -214,7 +224,9 @@ func parseCommandLineOptions() RuntimeOptions { flag.StringVar(&options.generateDir, "generate", "", "Generate key and config in specified dir, then exit") flag.StringVar(&options.guiAddress, "gui-address", options.guiAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")") flag.StringVar(&options.guiAPIKey, "gui-apikey", options.guiAPIKey, "Override GUI API key") - flag.StringVar(&options.confDir, "home", "", "Set configuration directory") + flag.StringVar(&options.homeDir, "home", "", "Set configuration and data directory") + flag.StringVar(&options.confDir, "config", "", "Set configuration directory (config and keys)") + flag.StringVar(&options.dataDir, "data", "", "Set data directory (database and logs)") flag.IntVar(&options.logFlags, "logflags", options.logFlags, "Select information in log line prefix (see below)") flag.BoolVar(&options.noBrowser, "no-browser", false, "Do not start browser") flag.BoolVar(&options.browserOnly, "browser-only", false, "Open GUI in browser") @@ -232,7 +244,7 @@ func parseCommandLineOptions() RuntimeOptions { flag.BoolVar(&options.Verbose, "verbose", false, "Print verbose log output") flag.BoolVar(&options.paused, "paused", false, "Start with all devices and folders paused") flag.BoolVar(&options.unpaused, "unpaused", false, "Start with all devices and folders unpaused") - flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (still always logs to stdout).") + flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (see below).") flag.IntVar(&options.logMaxSize, "log-max-size", options.logMaxSize, "Maximum size of any file (zero to disable log rotation).") flag.IntVar(&options.logMaxFiles, "log-max-old-files", options.logMaxFiles, "Number of old files to keep (zero to keep only current).") flag.StringVar(&options.auditFile, "auditfile", options.auditFile, "Specify audit file (use \"-\" for stdout, \"--\" for stderr)") @@ -254,6 +266,17 @@ func parseCommandLineOptions() RuntimeOptions { return options } +func setLocation(enum locations.BaseDirEnum, loc string) error { + if !filepath.IsAbs(loc) { + var err error + loc, err = filepath.Abs(loc) + if err != nil { + return err + } + } + return locations.SetBaseDir(enum, loc) +} + func main() { options := parseCommandLineOptions() l.SetFlags(options.logFlags) @@ -271,25 +294,33 @@ func main() { osutil.HideConsole() } - if options.confDir != "" { - // Not set as default above because the string can be really long. - if !filepath.IsAbs(options.confDir) { - var err error - options.confDir, err = filepath.Abs(options.confDir) - if err != nil { - l.Warnln("Failed to make options path absolute:", err) - os.Exit(syncthing.ExitError.AsInt()) - } + // Not set as default above because the strings can be really long. + var err error + homeSet := options.homeDir != "" + confSet := options.confDir != "" + dataSet := options.dataDir != "" + switch { + case dataSet != confSet: + err = errors.New("either both or none of -conf and -data must be given, use -home to set both at once") + case homeSet && dataSet: + err = errors.New("-home must not be used together with -conf and -data") + case homeSet: + if err = setLocation(locations.ConfigBaseDir, options.homeDir); err == nil { + err = setLocation(locations.DataBaseDir, options.homeDir) } - if err := locations.SetBaseDir(locations.ConfigBaseDir, options.confDir); err != nil { - l.Warnln(err) - os.Exit(syncthing.ExitError.AsInt()) + case dataSet: + if err = setLocation(locations.ConfigBaseDir, options.confDir); err == nil { + err = setLocation(locations.DataBaseDir, options.dataDir) } } + if err != nil { + l.Warnln("Command line options:", err) + os.Exit(syncthing.ExitError.AsInt()) + } - if options.logFile == "" { - // Blank means use the default logfile location. We must set this - // *after* expandLocations above. + if options.logFile == "default" || options.logFile == "" { + // We must set this *after* expandLocations above. + // Handling an empty value is for backwards compatibility (<1.4.1). options.logFile = locations.Get(locations.LogFile) } diff --git a/etc/linux-desktop/syncthing-start.desktop b/etc/linux-desktop/syncthing-start.desktop index b84f6dce9..17051a576 100644 --- a/etc/linux-desktop/syncthing-start.desktop +++ b/etc/linux-desktop/syncthing-start.desktop @@ -2,7 +2,7 @@ Name=Start Syncthing GenericName=File synchronization Comment=Starts the main syncthing process in the background. -Exec=/usr/bin/syncthing -no-browser -logfile=~/.config/syncthing/syncthing.log +Exec=/usr/bin/syncthing -no-browser -logfile=default Icon=syncthing Terminal=false Type=Application diff --git a/lib/locations/locations.go b/lib/locations/locations.go index 87f889c79..31b1ddfca 100644 --- a/lib/locations/locations.go +++ b/lib/locations/locations.go @@ -39,11 +39,23 @@ const ( type BaseDirEnum string const ( + // Overridden by -home flag ConfigBaseDir BaseDirEnum = "config" - HomeBaseDir BaseDirEnum = "home" + DataBaseDir BaseDirEnum = "data" + // User's home directory, *not* -home flag + UserHomeBaseDir BaseDirEnum = "userHome" ) +// Platform dependent directories +var baseDirs = make(map[BaseDirEnum]string, 3) + func init() { + userHome := userHomeDir() + config := defaultConfigDir(userHome) + baseDirs[UserHomeBaseDir] = userHome + baseDirs[ConfigBaseDir] = config + baseDirs[DataBaseDir] = defaultDataDir(userHome, config) + err := expandLocations() if err != nil { fmt.Println(err) @@ -68,11 +80,7 @@ func GetBaseDir(baseDir BaseDirEnum) string { return baseDirs[baseDir] } -// Platform dependent directories -var baseDirs = map[BaseDirEnum]string{ - ConfigBaseDir: defaultConfigDir(), // Overridden by -home flag - HomeBaseDir: homeDir(), // User's home directory, *not* -home flag -} +var databaseDirname = "index-v0.14.0.db" // Use the variables from baseDirs here var locationTemplates = map[LocationEnum]string{ @@ -81,13 +89,13 @@ var locationTemplates = map[LocationEnum]string{ KeyFile: "${config}/key.pem", HTTPSCertFile: "${config}/https-cert.pem", HTTPSKeyFile: "${config}/https-key.pem", - Database: "${config}/index-v0.14.0.db", - LogFile: "${config}/syncthing.log", // -logfile on Windows - CsrfTokens: "${config}/csrftokens.txt", - PanicLog: "${config}/panic-${timestamp}.log", - AuditLog: "${config}/audit-${timestamp}.log", + Database: "${data}/" + databaseDirname, + LogFile: "${data}/syncthing.log", // -logfile on Windows + CsrfTokens: "${data}/csrftokens.txt", + PanicLog: "${data}/panic-${timestamp}.log", + AuditLog: "${data}/audit-${timestamp}.log", GUIAssets: "${config}/gui", - DefFolder: "${home}/Sync", + DefFolder: "${userHome}/Sync", } var locations = make(map[LocationEnum]string) @@ -114,7 +122,7 @@ func expandLocations() error { // defaultConfigDir returns the default configuration directory, as figured // out by various the environment variables present on each platform, or dies // trying. -func defaultConfigDir() string { +func defaultConfigDir(userHome string) string { switch runtime.GOOS { case "windows": if p := os.Getenv("LocalAppData"); p != "" { @@ -123,34 +131,53 @@ func defaultConfigDir() string { return filepath.Join(os.Getenv("AppData"), "Syncthing") case "darwin": - dir, err := fs.ExpandTilde("~/Library/Application Support/Syncthing") - if err != nil { - fmt.Println(err) - panic("Failed to get default config dir") - } - return dir + return filepath.Join(userHome, "Library/Application Support/Syncthing") default: if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" { return filepath.Join(xdgCfg, "syncthing") } - dir, err := fs.ExpandTilde("~/.config/syncthing") - if err != nil { - fmt.Println(err) - panic("Failed to get default config dir") - } - return dir + return filepath.Join(userHome, ".config/syncthing") } } -// homeDir returns the user's home directory, or dies trying. -func homeDir() string { - home, err := fs.ExpandTilde("~") +// defaultDataDir returns the default data directory, which usually is the +// config directory but might be something else. +func defaultDataDir(userHome, config string) string { + switch runtime.GOOS { + case "windows", "darwin": + return config + + default: + // If a database exists at the "normal" location, use that anyway. + if _, err := os.Lstat(filepath.Join(config, databaseDirname)); err == nil { + return config + } + // Always use this env var, as it's explicitly set by the user + if xdgHome := os.Getenv("XDG_DATA_HOME"); xdgHome != "" { + return filepath.Join(xdgHome, "syncthing") + } + // Only use the XDG default, if a syncthing specific dir already + // exists. Existence of ~/.local/share is not deemed enough, as + // it may also exist erroneously on non-XDG systems. + xdgDefault := filepath.Join(userHome, ".local/share/syncthing") + if _, err := os.Lstat(xdgDefault); err == nil { + return xdgDefault + } + // FYI: XDG_DATA_DIRS is not relevant, as it is for system-wide + // data dirs, not user specific ones. + return config + } +} + +// userHomeDir returns the user's home directory, or dies trying. +func userHomeDir() string { + userHome, err := fs.ExpandTilde("~") if err != nil { fmt.Println(err) panic("Failed to get user home dir") } - return home + return userHome } func GetTimestamped(key LocationEnum) string {