diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 59d92dff8b2..a4055b8bcc8 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -9,6 +9,7 @@ package main import ( "bytes" "crypto/tls" + "encoding/json" "errors" "flag" "fmt" @@ -225,34 +226,35 @@ var ( ) type RuntimeOptions struct { - confDir string - resetDatabase bool - resetDeltaIdxs bool - showVersion bool - showPaths bool - showDeviceId bool - doUpgrade bool - doUpgradeCheck bool - upgradeTo string - noBrowser bool - browserOnly bool - hideConsole bool - logFile string - auditEnabled bool - auditFile string - verbose bool - paused bool - unpaused bool - guiAddress string - guiAPIKey string - generateDir string - noRestart bool - profiler string - assetDir string - cpuProfile bool - stRestarting bool - logFlags int - showHelp bool + confDir string + resetDatabase bool + resetDeltaIdxs bool + showVersion bool + showPaths bool + showDeviceId bool + doUpgrade bool + doUpgradeCheck bool + upgradeTo string + noBrowser bool + browserOnly bool + hideConsole bool + logFile string + auditEnabled bool + auditFile string + verbose bool + paused bool + unpaused bool + guiAddress string + guiAPIKey string + generateDir string + noRestart bool + profiler string + assetDir string + cpuProfile bool + stRestarting bool + logFlags int + showHelp bool + allowConfigurationDowngrade bool } func defaultRuntimeOptions() RuntimeOptions { @@ -306,6 +308,7 @@ func parseCommandLineOptions() RuntimeOptions { 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). Cannot be used together with -no-restart/STNORESTART environment variable.") flag.StringVar(&options.auditFile, "auditfile", options.auditFile, "Specify audit file (use \"-\" for stdout, \"--\" for stderr)") + flag.BoolVar(&options.allowConfigurationDowngrade, "allow-configuration-downgrade", false, "Downgrade configuration to current Syncthing configuration version") if runtime.GOOS == "windows" { // Allow user to hide the console window flag.BoolVar(&options.hideConsole, "no-console", false, "Hide console window") @@ -663,7 +666,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) { "myID": myID.String(), }) - cfg := loadConfigAtStartup() + var cfg *config.Wrapper + if runtimeOptions.allowConfigurationDowngrade { + cfg = downgradeConfig() + } else { + cfg = loadConfigAtStartup() + } if err := checkShortIDs(cfg); err != nil { l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one of the following:\n ", err) @@ -748,7 +756,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) { curParts := strings.Split(Version, "-") if prevParts[0] != curParts[0] { if prevVersion != "" { - l.Infoln("Detected upgrade from", prevVersion, "to", Version) + // Check Syncthing version + checkRelation := upgrade.CompareVersions(curParts[0], prevParts[0]) + if checkRelation == upgrade.Older || checkRelation == upgrade.MajorOlder { + l.Warnf("Downgrade detected: %s to %s", prevVersion, Version) + } else { + l.Infoln("Detected upgrade from", prevVersion, "to", Version) + } } // Drop delta indexes in case we've changed random stuff we @@ -972,15 +986,97 @@ func loadConfigAtStartup() *config.Wrapper { } if cfg.RawCopy().OriginalVersion != config.CurrentVersion { + // Check config version + if cfg.RawCopy().OriginalVersion > config.CurrentVersion { + // Config version is not compatible with installed version + if findOldConfig(cfg) { + archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", config.CurrentVersion) + l.Infof("Compatible version of config was found in path %s, can be used manually.", archivePath) + } + l.Infof("Use `-allow-downgrade-config' flag to downgrade your config.") + l.Fatalf("Current Configuration Version: %d, Loading Configuration Version: %d (Configuration Version Mismatch)", cfg.RawCopy().OriginalVersion, config.CurrentVersion) + } err = archiveAndSaveConfig(cfg) if err != nil { l.Fatalln("Config archive:", err) } } + return cfg +} + +func downgradeConfig() *config.Wrapper { + cfgFile := locations[locConfigFile] + cfg, err := config.Load(cfgFile, myID) + if os.IsNotExist(err) { + cfg = defaultConfig(cfgFile) + cfg.Save() + l.Infof("Default config saved. Edit %s to taste or use the GUI\n", cfg.ConfigPath()) + } else if err == io.EOF { + l.Fatalln("Failed to load config: unexpected end of file. Truncated or empty configuration?") + } else if err != nil { + l.Fatalln("Failed to load config:", err) + } + + configVersion := cfg.RawCopy().OriginalVersion + // Check config version + if configVersion > config.CurrentVersion { + // Config version is not compatible with installed version + l.Infof("Current Configuration Version: %d, Loading Configuration Version: %d (Configuration Version Mismatch)", configVersion, config.CurrentVersion) + l.Infof("Wait for downgrading...") + + fd, err := os.Open(cfgFile) + if err != nil { + l.Fatalln("Config not opened") + } + defer fd.Close() + + // Read XML and set defaults (Check-1) + // In most cases everything should correct on here except Version. + cfgXML, err := config.ReadXML(fd, myID) + if err != nil { + l.Fatalln("Config not read") + } + + err = archiveAndSaveConfig(cfg) + if err != nil { + l.Fatalln("Config archive:", err) + } + + // Read configuration second time and Unmarshal to configuration struct (Check-2) + var cfgConfig config.Configuration + var buf bytes.Buffer + err = json.NewEncoder(&buf).Encode(cfgXML) + if err != nil { + l.Fatalln("Config didn't downgrade: ", err) + } + + cfgConfig, err = config.ReadJSON(&buf, myID) + if err != nil { + l.Fatalln("Config didn't downgrade: ", err) + } + + // Change version + cfgConfig.Version = config.CurrentVersion + cfgConfig.OriginalVersion = config.CurrentVersion + cfgConfigWrapper := config.Wrap(cfgFile, cfgConfig) + + cfgConfigWrapper.Save() + return cfgConfigWrapper + } + l.Infoln("No need to use `-allow-downgrade-config` flag") return cfg } +func findOldConfig(cfg *config.Wrapper) bool { + archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", config.CurrentVersion) + // Check if file already exists + if _, err := os.Stat(archivePath); err == nil { + return true + } + return false +} + func archiveAndSaveConfig(cfg *config.Wrapper) error { // Copy the existing config to an archive copy archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.RawCopy().OriginalVersion)