diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 98c65d19..679ac1cf 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -37,6 +37,7 @@ import ( "github.com/syncthing/syncthing/internal/events" "github.com/syncthing/syncthing/internal/logger" "github.com/syncthing/syncthing/internal/model" + "github.com/syncthing/syncthing/internal/osutil" "github.com/syncthing/syncthing/internal/protocol" "github.com/syncthing/syncthing/internal/upgrade" "github.com/vitrun/qart/qr" @@ -253,12 +254,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) { var folder = qs.Get("folder") var res = make(map[string]interface{}) - for _, cr := range cfg.Folders { - if cr.ID == folder { - res["invalid"] = cr.Invalid - break - } - } + res["invalid"] = cfg.Folders()[folder].Invalid globalFiles, globalDeleted, globalBytes := m.GlobalSize(folder) res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes @@ -308,7 +304,7 @@ func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request) func restGetConfig(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - json.NewEncoder(w).Encode(cfg) + json.NewEncoder(w).Encode(cfg.Raw()) } func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { @@ -319,7 +315,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } else { - if newCfg.GUI.Password != cfg.GUI.Password { + if newCfg.GUI.Password != cfg.GUI().Password { if newCfg.GUI.Password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0) if err != nil { @@ -334,7 +330,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { // Start or stop usage reporting as appropriate - if newCfg.Options.URAccepted > cfg.Options.URAccepted { + if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc { // UR was enabled newCfg.Options.URAccepted = usageReportVersion err := sendUsageReport(m) @@ -342,7 +338,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { l.Infoln("Usage report:", err) } go usageReportingLoop(m) - } else if newCfg.Options.URAccepted < cfg.Options.URAccepted { + } else if newCfg.Options.URAccepted < curAcc { // UR was disabled newCfg.Options.URAccepted = -1 stopUsageReporting() @@ -350,10 +346,8 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { // Activate and save - configInSync = !config.ChangeRequiresRestart(cfg, newCfg) - newCfg.Location = cfg.Location - newCfg.Save() - cfg = newCfg + cfg.Replace(newCfg) + cfg.Save() } } @@ -391,13 +385,14 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) { var m runtime.MemStats runtime.ReadMemStats(&m) + tilde, _ := osutil.ExpandTilde("~") res := make(map[string]interface{}) res["myID"] = myID.String() res["goroutines"] = runtime.NumGoroutine() res["alloc"] = m.Alloc res["sys"] = m.Sys - m.HeapReleased - res["tilde"] = expandTilde("~") - if cfg.Options.GlobalAnnEnabled && discoverer != nil { + res["tilde"] = tilde + if cfg.Options().GlobalAnnEnabled && discoverer != nil { res["extAnnounceOK"] = discoverer.ExtAnnounceOK() } cpuUsageLock.RLock() @@ -606,7 +601,7 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques tot := map[string]float64{} count := map[string]float64{} - for _, folder := range cfg.Folders { + for _, folder := range cfg.Folders() { for _, device := range folder.DeviceIDs() { deviceStr := device.String() if m.ConnectedTo(device) { diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index f1c748c6..e0256744 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -26,6 +26,7 @@ import ( "net" "net/http" _ "net/http/pprof" + "net/url" "os" "path/filepath" "regexp" @@ -44,6 +45,7 @@ import ( "github.com/syncthing/syncthing/internal/files" "github.com/syncthing/syncthing/internal/logger" "github.com/syncthing/syncthing/internal/model" + "github.com/syncthing/syncthing/internal/osutil" "github.com/syncthing/syncthing/internal/protocol" "github.com/syncthing/syncthing/internal/upgrade" "github.com/syncthing/syncthing/internal/upnp" @@ -94,7 +96,7 @@ func init() { } var ( - cfg config.Configuration + cfg *config.ConfigWrapper myID protocol.DeviceID confDir string logFlags int = log.Ltime @@ -184,7 +186,11 @@ var ( ) func main() { - flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory") + defConfDir, err := getDefaultConfDir() + if err != nil { + l.Fatalln("home:", err) + } + flag.StringVar(&confDir, "home", defConfDir, "Set configuration directory") flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster") flag.BoolVar(&showVersion, "version", false, "Show version") flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade") @@ -206,7 +212,10 @@ func main() { l.SetFlags(logFlags) if generateDir != "" { - dir := expandTilde(generateDir) + dir, err := osutil.ExpandTilde(generateDir) + if err != nil { + l.Fatalln("generate:", err) + } info, err := os.Stat(dir) if err != nil { @@ -234,7 +243,10 @@ func main() { return } - confDir = expandTilde(confDir) + confDir, err := osutil.ExpandTilde(confDir) + if err != nil { + l.Fatalln("home:", err) + } if info, err := os.Stat(confDir); err == nil && !info.IsDir() { l.Fatalln("Config directory", confDir, "is not a directory") @@ -299,27 +311,6 @@ func syncthingMain() { events.Default.Log(events.Starting, map[string]string{"home": confDir}) - if _, err = os.Stat(confDir); err != nil && confDir == getDefaultConfDir() { - // We are supposed to use the default configuration directory. It - // doesn't exist. In the past our default has been ~/.syncthing, so if - // that directory exists we move it to the new default location and - // continue. We don't much care if this fails at this point, we will - // be checking that later. - - var oldDefault string - if runtime.GOOS == "windows" { - oldDefault = filepath.Join(os.Getenv("AppData"), "Syncthing") - } else { - oldDefault = expandTilde("~/.syncthing") - } - if _, err := os.Stat(oldDefault); err == nil { - os.MkdirAll(filepath.Dir(confDir), 0700) - if err := os.Rename(oldDefault, confDir); err == nil { - l.Infoln("Moved config dir", oldDefault, "to", confDir) - } - } - } - // Ensure that that we have a certificate and key. cert, err = loadCert(confDir, "") if err != nil { @@ -347,8 +338,8 @@ func syncthingMain() { cfg, err = config.Load(cfgFile, myID) if err == nil { - myCfg := cfg.GetDeviceConfiguration(myID) - if myCfg == nil || myCfg.Name == "" { + myCfg := cfg.Devices()[myID] + if myCfg.Name == "" { myName, _ = os.Hostname() } else { myName = myCfg.Name @@ -356,10 +347,13 @@ func syncthingMain() { } else { l.Infoln("No config file; starting with empty defaults") myName, _ = os.Hostname() - defaultFolder := filepath.Join(getHomeDir(), "Sync") + defaultFolder, err := osutil.ExpandTilde("~/Sync") + if err != nil { + l.Fatalln("home:", err) + } - cfg = config.New(cfgFile, myID) - cfg.Folders = []config.FolderConfiguration{ + newCfg := config.New(myID) + newCfg.Folders = []config.FolderConfiguration{ { ID: "default", Path: defaultFolder, @@ -367,7 +361,7 @@ func syncthingMain() { Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}}, }, } - cfg.Devices = []config.DeviceConfiguration{ + newCfg.Devices = []config.DeviceConfiguration{ { DeviceID: myID, Addresses: []string{"dynamic"}, @@ -379,14 +373,15 @@ func syncthingMain() { if err != nil { l.Fatalln("get free port (GUI):", err) } - cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port) + newCfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port) port, err = getFreePort("0.0.0.0", 22000) if err != nil { l.Fatalln("get free port (BEP):", err) } - cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)} + newCfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)} + cfg = config.Wrap(cfgFile, newCfg) cfg.Save() l.Infof("Edit %s to taste or use the GUI\n", cfgFile) } @@ -418,11 +413,13 @@ func syncthingMain() { // If the read or write rate should be limited, set up a rate limiter for it. // This will be used on connections created in the connect and listen routines. - if cfg.Options.MaxSendKbps > 0 { - writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps)) + opts := cfg.Options() + + if opts.MaxSendKbps > 0 { + writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps)) } - if cfg.Options.MaxRecvKbps > 0 { - readRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxRecvKbps), int64(5*1000*cfg.Options.MaxRecvKbps)) + if opts.MaxRecvKbps > 0 { + readRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxRecvKbps), int64(5*1000*opts.MaxRecvKbps)) } // If this is the first time the user runs v0.9, archive the old indexes and config. @@ -434,33 +431,36 @@ func syncthingMain() { } // Remove database entries for folders that no longer exist in the config - folderMap := cfg.FolderMap() + folders := cfg.Folders() for _, folder := range files.ListFolders(db) { - if _, ok := folderMap[folder]; !ok { + if _, ok := folders[folder]; !ok { l.Infof("Cleaning data for dropped folder %q", folder) files.DropFolder(db, folder) } } - m := model.NewModel(&cfg, myName, "syncthing", Version, db) + m := model.NewModel(cfg, myName, "syncthing", Version, db) nextFolder: - for i, folder := range cfg.Folders { + for id, folder := range cfg.Folders() { if folder.Invalid != "" { continue } - folder.Path = expandTilde(folder.Path) + folder.Path, err = osutil.ExpandTilde(folder.Path) + if err != nil { + l.Fatalln("home:", err) + } m.AddFolder(folder) fi, err := os.Stat(folder.Path) - if m.CurrentLocalVersion(folder.ID) > 0 { + if m.CurrentLocalVersion(id) > 0 { // Safety check. If the cached index contains files but the // folder doesn't exist, we have a problem. We would assume // that all files have been deleted which might not be the case, // so mark it as invalid instead. if err != nil || !fi.IsDir() { l.Warnf("Stopping folder %q - path does not exist, but has files in index", folder.ID) - cfg.Folders[i].Invalid = "folder path missing" + cfg.InvalidateFolder(id, "folder path missing") continue nextFolder } } else if os.IsNotExist(err) { @@ -473,14 +473,14 @@ nextFolder: // If there was another error or we could not create the // path, the folder is invalid. l.Warnf("Stopping folder %q - %v", err) - cfg.Folders[i].Invalid = err.Error() + cfg.InvalidateFolder(id, err.Error()) continue nextFolder } } // GUI - guiCfg := overrideGUIConfig(cfg.GUI, guiAddress, guiAuthentication, guiAPIKey) + guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey) if guiCfg.Enabled && guiCfg.Address != "" { addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address) @@ -511,7 +511,7 @@ nextFolder: if err != nil { l.Fatalln("Cannot start GUI:", err) } - if !noBrowser && cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 { + if !noBrowser && opts.StartBrowser && len(os.Getenv("STRESTART")) == 0 { urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port))) openURL(urlOpen) } @@ -521,7 +521,7 @@ nextFolder: // Clear out old indexes for other devices. Otherwise we'll start up and // start needing a bunch of files which are nowhere to be found. This // needs to be changed when we correctly do persistent indexes. - for _, folderCfg := range cfg.Folders { + for _, folderCfg := range cfg.Folders() { if folderCfg.Invalid != "" { continue } @@ -536,8 +536,11 @@ nextFolder: // Remove all .idx* files that don't belong to an active folder. validIndexes := make(map[string]bool) - for _, folder := range cfg.Folders { - dir := expandTilde(folder.Path) + for _, folder := range cfg.Folders() { + dir, err := osutil.ExpandTilde(folder.Path) + if err != nil { + l.Fatalln("home:", err) + } id := fmt.Sprintf("%x", sha1.Sum([]byte(dir))) validIndexes[id] = true } @@ -558,7 +561,7 @@ nextFolder: // The default port we announce, possibly modified by setupUPnP next. - addr, err := net.ResolveTCPAddr("tcp", cfg.Options.ListenAddress[0]) + addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0]) if err != nil { l.Fatalln("Bad listen address:", err) } @@ -566,7 +569,7 @@ nextFolder: // UPnP - if cfg.Options.UPnPEnabled { + if opts.UPnPEnabled { setupUPnP() } @@ -574,7 +577,7 @@ nextFolder: discoverer = discovery(externalPort) go listenConnect(myID, m, tlsCfg) - for _, folder := range cfg.Folders { + for _, folder := range cfg.Folders() { if folder.Invalid != "" { continue } @@ -599,17 +602,18 @@ nextFolder: defer pprof.StopCPUProfile() } - for _, device := range cfg.Devices { + for _, device := range cfg.Devices() { if len(device.Name) > 0 { l.Infof("Device %s is %q at %v", device.DeviceID, device.Name, device.Addresses) } } - if cfg.Options.URAccepted > 0 && cfg.Options.URAccepted < usageReportVersion { + if opts.URAccepted > 0 && opts.URAccepted < usageReportVersion { l.Infoln("Anonymous usage report has changed; revoking acceptance") - cfg.Options.URAccepted = 0 + opts.URAccepted = 0 + cfg.SetOptions(opts) } - if cfg.Options.URAccepted >= usageReportVersion { + if opts.URAccepted >= usageReportVersion { go usageReportingLoop(m) go func() { time.Sleep(10 * time.Minute) @@ -620,11 +624,11 @@ nextFolder: }() } - if cfg.Options.RestartOnWakeup { + if opts.RestartOnWakeup { go standbyMonitor() } - if cfg.Options.AutoUpgradeIntervalH > 0 { + if opts.AutoUpgradeIntervalH > 0 { go autoUpgrade() } @@ -645,8 +649,8 @@ func generateEvents() { } func setupUPnP() { - if len(cfg.Options.ListenAddress) == 1 { - _, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0]) + if opts := cfg.Options(); len(opts.ListenAddress) == 1 { + _, portStr, err := net.SplitHostPort(opts.ListenAddress[0]) if err != nil { l.Warnln("Bad listen address:", err) } else { @@ -666,7 +670,7 @@ func setupUPnP() { l.Debugf("UPnP: %v", err) } } - if cfg.Options.UPnPRenewal > 0 { + if opts.UPnPRenewal > 0 { go renewUPnP(port) } } @@ -681,7 +685,7 @@ func setupExternalPort(igd *upnp.IGD, port int) int { rnd := rand.NewSource(certSeed(cert.Certificate[0])) for i := 0; i < 10; i++ { r := 1024 + int(rnd.Int63()%(65535-1024)) - err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options.UPnPLease*60) + err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options().UPnPLease*60) if err == nil { return r } @@ -691,7 +695,8 @@ func setupExternalPort(igd *upnp.IGD, port int) int { func renewUPnP(port int) { for { - time.Sleep(time.Duration(cfg.Options.UPnPRenewal) * time.Minute) + opts := cfg.Options() + time.Sleep(time.Duration(opts.UPnPRenewal) * time.Minute) igd, err := upnp.Discover() if err != nil { @@ -700,7 +705,7 @@ func renewUPnP(port int) { // Just renew the same port that we already have if externalPort != 0 { - err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", cfg.Options.UPnPLease*60) + err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60) if err == nil { l.Infoln("Renewed UPnP port mapping - external port", externalPort) continue @@ -715,7 +720,7 @@ func renewUPnP(port int) { externalPort = r l.Infoln("Updated UPnP port mapping - external port", externalPort) discoverer.StopGlobal() - discoverer.StartGlobal(cfg.Options.GlobalAnnServer, uint16(r)) + discoverer.StartGlobal(opts.GlobalAnnServer, uint16(r)) continue } l.Warnln("Failed to update UPnP port mapping - external port", externalPort) @@ -724,7 +729,7 @@ func renewUPnP(port int) { func resetFolders() { suffix := fmt.Sprintf(".syncthing-reset-%d", time.Now().UnixNano()) - for _, folder := range cfg.Folders { + for _, folder := range cfg.Folders() { if _, err := os.Stat(folder.Path); err == nil { l.Infof("Reset: Moving %s -> %s", folder.Path, folder.Path+suffix) os.Rename(folder.Path, folder.Path+suffix) @@ -785,7 +790,7 @@ func listenConnect(myID protocol.DeviceID, m *model.Model, tlsCfg *tls.Config) { var conns = make(chan *tls.Conn) // Listen - for _, addr := range cfg.Options.ListenAddress { + for _, addr := range cfg.Options().ListenAddress { go listenTLS(conns, addr, tlsCfg) } @@ -815,8 +820,8 @@ next: continue } - for _, deviceCfg := range cfg.Devices { - if deviceCfg.DeviceID == remoteID { + for deviceID, deviceCfg := range cfg.Devices() { + if deviceID == remoteID { // Verify the name on the certificate. By default we set it to // "syncthing" when generating, but the user may have replaced // the certificate and used another name. @@ -917,12 +922,12 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) { var delay time.Duration = 1 * time.Second for { nextDevice: - for _, deviceCfg := range cfg.Devices { - if deviceCfg.DeviceID == myID { + for deviceID, deviceCfg := range cfg.Devices() { + if deviceID == myID { continue } - if m.ConnectedTo(deviceCfg.DeviceID) { + if m.ConnectedTo(deviceID) { continue } @@ -930,7 +935,7 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) { for _, addr := range deviceCfg.Addresses { if addr == "dynamic" { if discoverer != nil { - t := discoverer.Lookup(deviceCfg.DeviceID) + t := discoverer.Lookup(deviceID) if len(t) == 0 { continue } @@ -987,7 +992,7 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) { time.Sleep(delay) delay *= 2 - if maxD := time.Duration(cfg.Options.ReconnectIntervalS) * time.Second; delay > maxD { + if maxD := time.Duration(cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD { delay = maxD } } @@ -1010,16 +1015,17 @@ func setTCPOptions(conn *net.TCPConn) { } func discovery(extPort int) *discover.Discoverer { - disc := discover.NewDiscoverer(myID, cfg.Options.ListenAddress) + opts := cfg.Options() + disc := discover.NewDiscoverer(myID, opts.ListenAddress) - if cfg.Options.LocalAnnEnabled { + if opts.LocalAnnEnabled { l.Infoln("Starting local discovery announcements") - disc.StartLocal(cfg.Options.LocalAnnPort, cfg.Options.LocalAnnMCAddr) + disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr) } - if cfg.Options.GlobalAnnEnabled { + if opts.GlobalAnnEnabled { l.Infoln("Starting global discovery announcements") - disc.StartGlobal(cfg.Options.GlobalAnnServer, uint16(extPort)) + disc.StartGlobal(opts.GlobalAnnServer, uint16(extPort)) } return disc @@ -1041,56 +1047,23 @@ func ensureDir(dir string, mode int) { } } -func getDefaultConfDir() string { +func getDefaultConfDir() (string, error) { switch runtime.GOOS { case "windows": - return filepath.Join(os.Getenv("LocalAppData"), "Syncthing") + return filepath.Join(os.Getenv("LocalAppData"), "Syncthing"), nil case "darwin": - return expandTilde("~/Library/Application Support/Syncthing") + return osutil.ExpandTilde("~/Library/Application Support/Syncthing") default: if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" { - return filepath.Join(xdgCfg, "syncthing") + return filepath.Join(xdgCfg, "syncthing"), nil } else { - return expandTilde("~/.config/syncthing") + return osutil.ExpandTilde("~/.config/syncthing") } } } -func expandTilde(p string) string { - if p == "~" { - return getHomeDir() - } - - p = filepath.FromSlash(p) - if !strings.HasPrefix(p, fmt.Sprintf("~%c", os.PathSeparator)) { - return p - } - - return filepath.Join(getHomeDir(), p[2:]) -} - -func getHomeDir() string { - var home string - - switch runtime.GOOS { - case "windows": - home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath")) - if home == "" { - home = os.Getenv("UserProfile") - } - default: - home = os.Getenv("HOME") - } - - if home == "" { - l.Fatalln("No home directory found - set $HOME (or the platform equivalent).") - } - - return home -} - // getFreePort returns a free TCP port fort listening on. The ports given are // tried in succession and the first to succeed is returned. If none succeed, // a random high port is returned. @@ -1112,10 +1085,7 @@ func getFreePort(host string, ports ...int) (int, error) { return addr.Port, nil } -func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration { - // Make a copy of the config - cfg := originalCfg - +func overrideGUIConfig(cfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration { if address == "" { address = os.Getenv("STGUIADDRESS") } @@ -1123,16 +1093,25 @@ func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authenticat if address != "" { cfg.Enabled = true - addressParts := strings.SplitN(address, "://", 2) - switch addressParts[0] { - case "http": - cfg.UseTLS = false - case "https": - cfg.UseTLS = true - default: - l.Fatalln("Unidentified protocol", addressParts[0]) + if !strings.Contains(address, "//") { + // Assume just an IP was given. Don't touch he TLS setting. + cfg.Address = address + } else { + parsed, err := url.Parse(address) + if err != nil { + l.Fatalln(err) + } + l.Debugf("%#v", parsed) + switch parsed.Scheme { + case "http": + cfg.UseTLS = false + case "https": + cfg.UseTLS = true + default: + l.Fatalln("Unknown scheme:", parsed.Scheme) + } + cfg.Address = parsed.Host } - cfg.Address = addressParts[1] } if authentication == "" { @@ -1183,7 +1162,7 @@ func standbyMonitor() { func autoUpgrade() { var skipped bool - interval := time.Duration(cfg.Options.AutoUpgradeIntervalH) * time.Hour + interval := time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour for { if skipped { time.Sleep(interval) diff --git a/cmd/syncthing/usage_report.go b/cmd/syncthing/usage_report.go index 035e5733..fbbda181 100644 --- a/cmd/syncthing/usage_report.go +++ b/cmd/syncthing/usage_report.go @@ -42,13 +42,13 @@ func reportData(m *model.Model) map[string]interface{} { res["version"] = Version res["longVersion"] = LongVersion res["platform"] = runtime.GOOS + "-" + runtime.GOARCH - res["numFolders"] = len(cfg.Folders) - res["numDevices"] = len(cfg.Devices) + res["numFolders"] = len(cfg.Folders()) + res["numDevices"] = len(cfg.Devices()) var totFiles, maxFiles int var totBytes, maxBytes int64 - for _, folder := range cfg.Folders { - files, _, bytes := m.GlobalSize(folder.ID) + for folderID := range cfg.Folders() { + files, _, bytes := m.GlobalSize(folderID) totFiles += files totBytes += bytes if files > maxFiles { diff --git a/internal/config/config.go b/internal/config/config.go index 9819d155..9d13cbdb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,28 +19,26 @@ package config import ( "encoding/xml" "fmt" + "io" "os" "reflect" "sort" "strconv" "code.google.com/p/go.crypto/bcrypt" - "github.com/syncthing/syncthing/internal/events" "github.com/syncthing/syncthing/internal/logger" - "github.com/syncthing/syncthing/internal/osutil" "github.com/syncthing/syncthing/internal/protocol" ) var l = logger.DefaultLogger type Configuration struct { - Location string `xml:"-" json:"-"` - Version int `xml:"version,attr" default:"5"` - Folders []FolderConfiguration `xml:"folder"` - Devices []DeviceConfiguration `xml:"device"` - GUI GUIConfiguration `xml:"gui"` - Options OptionsConfiguration `xml:"options"` - XMLName xml.Name `xml:"configuration" json:"-"` + Version int `xml:"version,attr" default:"5"` + Folders []FolderConfiguration `xml:"folder"` + Devices []DeviceConfiguration `xml:"device"` + GUI GUIConfiguration `xml:"gui"` + Options OptionsConfiguration `xml:"options"` + XMLName xml.Name `xml:"configuration" json:"-"` Deprecated_Repositories []FolderConfiguration `xml:"repository" json:"-"` Deprecated_Nodes []DeviceConfiguration `xml:"node" json:"-"` @@ -62,6 +60,15 @@ type FolderConfiguration struct { Deprecated_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"` } +func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID { + if r.deviceIDs == nil { + for _, n := range r.Devices { + r.deviceIDs = append(r.deviceIDs, n.DeviceID) + } + } + return r.deviceIDs +} + type VersioningConfiguration struct { Type string `xml:"type,attr"` Params map[string]string @@ -103,15 +110,6 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl return nil } -func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID { - if r.deviceIDs == nil { - for _, n := range r.Devices { - r.deviceIDs = append(r.deviceIDs, n.DeviceID) - } - } - return r.deviceIDs -} - type DeviceConfiguration struct { DeviceID protocol.DeviceID `xml:"id,attr"` Name string `xml:"name,attr,omitempty"` @@ -164,6 +162,42 @@ type GUIConfiguration struct { APIKey string `xml:"apikey,omitempty"` } +func New(myID protocol.DeviceID) Configuration { + var cfg Configuration + + setDefaults(&cfg) + setDefaults(&cfg.Options) + setDefaults(&cfg.GUI) + + cfg.prepare(myID) + + return cfg +} + +func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) { + var cfg Configuration + + setDefaults(&cfg) + setDefaults(&cfg.Options) + setDefaults(&cfg.GUI) + + err := xml.NewDecoder(r).Decode(&cfg) + + cfg.prepare(myID) + return cfg, err +} + +func (cfg *Configuration) WriteXML(w io.Writer) error { + e := xml.NewEncoder(w) + e.Indent("", " ") + err := e.Encode(cfg) + if err != nil { + return err + } + _, err = w.Write([]byte("\n")) + return err +} + func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration { m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices)) for _, n := range cfg.Devices { @@ -198,117 +232,6 @@ func (cfg *Configuration) FolderMap() map[string]FolderConfiguration { return m } -func setDefaults(data interface{}) error { - s := reflect.ValueOf(data).Elem() - t := s.Type() - - for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - tag := t.Field(i).Tag - - v := tag.Get("default") - if len(v) > 0 { - switch f.Interface().(type) { - case string: - f.SetString(v) - - case int: - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - f.SetInt(i) - - case bool: - f.SetBool(v == "true") - - case []string: - // We don't do anything with string slices here. Any default - // we set will be appended to by the XML decoder, so we fill - // those after decoding. - - default: - panic(f.Type()) - } - } - } - return nil -} - -// fillNilSlices sets default value on slices that are still nil. -func fillNilSlices(data interface{}) error { - s := reflect.ValueOf(data).Elem() - t := s.Type() - - for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - tag := t.Field(i).Tag - - v := tag.Get("default") - if len(v) > 0 { - switch f.Interface().(type) { - case []string: - if f.IsNil() { - rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1) - rv.Index(0).SetString(v) - f.Set(rv) - } - } - } - } - return nil -} - -func (cfg *Configuration) Save() error { - fd, err := os.Create(cfg.Location + ".tmp") - if err != nil { - l.Warnln("Saving config:", err) - return err - } - - e := xml.NewEncoder(fd) - e.Indent("", " ") - err = e.Encode(cfg) - if err != nil { - fd.Close() - return err - } - _, err = fd.Write([]byte("\n")) - - if err != nil { - l.Warnln("Saving config:", err) - fd.Close() - return err - } - - err = fd.Close() - if err != nil { - l.Warnln("Saving config:", err) - return err - } - - err = osutil.Rename(cfg.Location+".tmp", cfg.Location) - if err != nil { - l.Warnln("Saving config:", err) - } - events.Default.Log(events.ConfigSaved, cfg) - return err -} - -func uniqueStrings(ss []string) []string { - var m = make(map[string]bool, len(ss)) - for _, s := range ss { - m[s] = true - } - - var us = make([]string, 0, len(m)) - for k := range m { - us = append(us, k) - } - - return us -} - func (cfg *Configuration) prepare(myID protocol.DeviceID) { fillNilSlices(&cfg.Options) @@ -356,7 +279,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { cfg.Options.Deprecated_URDeclined = false cfg.Options.Deprecated_UREnabled = false - // Upgrade to v2 configuration if appropriate + // Upgrade to v1 configuration if appropriate if cfg.Version == 1 { convertV1V2(cfg) } @@ -421,41 +344,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { } } -func New(location string, myID protocol.DeviceID) Configuration { - var cfg Configuration - - cfg.Location = location - - setDefaults(&cfg) - setDefaults(&cfg.Options) - setDefaults(&cfg.GUI) - - cfg.prepare(myID) - - return cfg -} - -func Load(location string, myID protocol.DeviceID) (Configuration, error) { - var cfg Configuration - - cfg.Location = location - - setDefaults(&cfg) - setDefaults(&cfg.Options) - setDefaults(&cfg.GUI) - - fd, err := os.Open(location) - if err != nil { - return Configuration{}, err - } - err = xml.NewDecoder(fd).Decode(&cfg) - fd.Close() - - cfg.prepare(myID) - - return cfg, err -} - // ChangeRequiresRestart returns true if updating the configuration requires a // complete restart. func ChangeRequiresRestart(from, to Configuration) bool { @@ -593,29 +481,79 @@ func convertV1V2(cfg *Configuration) { cfg.Version = 2 } +func setDefaults(data interface{}) error { + s := reflect.ValueOf(data).Elem() + t := s.Type() -type DeviceConfigurationList []DeviceConfiguration + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + tag := t.Field(i).Tag -func (l DeviceConfigurationList) Less(a, b int) bool { - return l[a].DeviceID.Compare(l[b].DeviceID) == -1 -} -func (l DeviceConfigurationList) Swap(a, b int) { - l[a], l[b] = l[b], l[a] -} -func (l DeviceConfigurationList) Len() int { - return len(l) + v := tag.Get("default") + if len(v) > 0 { + switch f.Interface().(type) { + case string: + f.SetString(v) + + case int: + i, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return err + } + f.SetInt(i) + + case bool: + f.SetBool(v == "true") + + case []string: + // We don't do anything with string slices here. Any default + // we set will be appended to by the XML decoder, so we fill + // those after decoding. + + default: + panic(f.Type()) + } + } + } + return nil } -type FolderDeviceConfigurationList []FolderDeviceConfiguration +// fillNilSlices sets default value on slices that are still nil. +func fillNilSlices(data interface{}) error { + s := reflect.ValueOf(data).Elem() + t := s.Type() -func (l FolderDeviceConfigurationList) Less(a, b int) bool { - return l[a].DeviceID.Compare(l[b].DeviceID) == -1 + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + tag := t.Field(i).Tag + + v := tag.Get("default") + if len(v) > 0 { + switch f.Interface().(type) { + case []string: + if f.IsNil() { + rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1) + rv.Index(0).SetString(v) + f.Set(rv) + } + } + } + } + return nil } -func (l FolderDeviceConfigurationList) Swap(a, b int) { - l[a], l[b] = l[b], l[a] -} -func (l FolderDeviceConfigurationList) Len() int { - return len(l) + +func uniqueStrings(ss []string) []string { + var m = make(map[string]bool, len(ss)) + for _, s := range ss { + m[s] = true + } + + var us = make([]string, 0, len(m)) + for k := range m { + us = append(us, k) + } + + return us } func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration { @@ -664,3 +602,27 @@ loop: } return devices[0:count] } + +type DeviceConfigurationList []DeviceConfiguration + +func (l DeviceConfigurationList) Less(a, b int) bool { + return l[a].DeviceID.Compare(l[b].DeviceID) == -1 +} +func (l DeviceConfigurationList) Swap(a, b int) { + l[a], l[b] = l[b], l[a] +} +func (l DeviceConfigurationList) Len() int { + return len(l) +} + +type FolderDeviceConfigurationList []FolderDeviceConfiguration + +func (l FolderDeviceConfigurationList) Less(a, b int) bool { + return l[a].DeviceID.Compare(l[b].DeviceID) == -1 +} +func (l FolderDeviceConfigurationList) Swap(a, b int) { + l[a], l[b] = l[b], l[a] +} +func (l FolderDeviceConfigurationList) Len() int { + return len(l) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index eb488040..19655f99 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -52,7 +52,7 @@ func TestDefaultValues(t *testing.T) { KeepTemporariesH: 24, } - cfg := New("test", device1) + cfg := New(device1) if !reflect.DeepEqual(cfg.Options, expected) { t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options) @@ -60,11 +60,12 @@ func TestDefaultValues(t *testing.T) { } func TestDeviceConfig(t *testing.T) { - for i, ver := range []string{"v1", "v2", "v3", "v4", "v5"} { - cfg, err := Load("testdata/"+ver+".xml", device1) + for i, ver := range []string{"v3", "v4", "v5"} { + wr, err := Load("testdata/"+ver+".xml", device1) if err != nil { - t.Error(err) + t.Fatal(err) } + cfg := wr.cfg expectedFolders := []FolderConfiguration{ { @@ -120,8 +121,9 @@ func TestNoListenAddress(t *testing.T) { } expected := []string{""} - if !reflect.DeepEqual(cfg.Options.ListenAddress, expected) { - t.Errorf("Unexpected ListenAddress %#v", cfg.Options.ListenAddress) + actual := cfg.Options().ListenAddress + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Unexpected ListenAddress %#v", actual) } } @@ -150,30 +152,30 @@ func TestOverriddenValues(t *testing.T) { t.Error(err) } - if !reflect.DeepEqual(cfg.Options, expected) { + if !reflect.DeepEqual(cfg.Options(), expected) { t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options) } } func TestDeviceAddressesDynamic(t *testing.T) { name, _ := os.Hostname() - expected := []DeviceConfiguration{ - { + expected := map[protocol.DeviceID]DeviceConfiguration{ + device1: { DeviceID: device1, Addresses: []string{"dynamic"}, Compression: true, }, - { + device2: { DeviceID: device2, Addresses: []string{"dynamic"}, Compression: true, }, - { + device3: { DeviceID: device3, Addresses: []string{"dynamic"}, Compression: true, }, - { + device4: { DeviceID: device4, Name: name, // Set when auto created Addresses: []string{"dynamic"}, @@ -185,27 +187,28 @@ func TestDeviceAddressesDynamic(t *testing.T) { t.Error(err) } - if !reflect.DeepEqual(cfg.Devices, expected) { - t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, cfg.Devices) + actual := cfg.Devices() + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, actual) } } func TestDeviceAddressesStatic(t *testing.T) { name, _ := os.Hostname() - expected := []DeviceConfiguration{ - { + expected := map[protocol.DeviceID]DeviceConfiguration{ + device1: { DeviceID: device1, Addresses: []string{"192.0.2.1", "192.0.2.2"}, }, - { + device2: { DeviceID: device2, Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"}, }, - { + device3: { DeviceID: device3, Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"}, }, - { + device4: { DeviceID: device4, Name: name, // Set when auto created Addresses: []string{"dynamic"}, @@ -217,8 +220,9 @@ func TestDeviceAddressesStatic(t *testing.T) { t.Error(err) } - if !reflect.DeepEqual(cfg.Devices, expected) { - t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, cfg.Devices) + actual := cfg.Devices() + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, actual) } } @@ -228,7 +232,7 @@ func TestVersioningConfig(t *testing.T) { t.Error(err) } - vc := cfg.Folders[0].Versioning + vc := cfg.Folders()["test"].Versioning if vc.Type != "simple" { t.Errorf(`vc.Type %q != "simple"`, vc.Type) } @@ -254,10 +258,11 @@ func TestNewSaveLoad(t *testing.T) { return err == nil } - cfg := New(path, device1) + intCfg := New(device1) + cfg := Wrap(path, intCfg) // To make the equality pass later - cfg.XMLName.Local = "configuration" + cfg.cfg.XMLName.Local = "configuration" if exists(path) { t.Error(path, "exists") @@ -276,20 +281,8 @@ func TestNewSaveLoad(t *testing.T) { t.Error(err) } - if !reflect.DeepEqual(cfg, cfg2) { - t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2) - } - - cfg.GUI.User = "test" - cfg.Save() - - cfg2, err = Load(path, device1) - if err != nil { - t.Error(err) - } - - if cfg2.GUI.User != "test" || !reflect.DeepEqual(cfg, cfg2) { - t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2) + if !reflect.DeepEqual(cfg.Raw(), cfg2.Raw()) { + t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg.Raw(), cfg2.Raw()) } os.Remove(path) diff --git a/internal/config/wrapper.go b/internal/config/wrapper.go new file mode 100644 index 00000000..4c2fd6f8 --- /dev/null +++ b/internal/config/wrapper.go @@ -0,0 +1,260 @@ +// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +package config + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/syncthing/syncthing/internal/events" + "github.com/syncthing/syncthing/internal/osutil" + "github.com/syncthing/syncthing/internal/protocol" +) + +// An interface to handle configuration changes, and a wrapper type รก la +// http.Handler + +type Handler interface { + Changed(Configuration) error +} + +type HandlerFunc func(Configuration) error + +func (fn HandlerFunc) Changed(cfg Configuration) error { + return fn(cfg) +} + +// A wrapper around a Configuration that manages loads, saves and published +// notifications of changes to registered Handlers + +type ConfigWrapper struct { + cfg Configuration + path string + + deviceMap map[protocol.DeviceID]DeviceConfiguration + folderMap map[string]FolderConfiguration + replaces chan Configuration + subs []Handler + mut sync.Mutex +} + +// Wrap wraps an existing Configuration structure and ties it to a file on +// disk. +func Wrap(path string, cfg Configuration) *ConfigWrapper { + w := &ConfigWrapper{cfg: cfg, path: path} + w.replaces = make(chan Configuration) + go w.Serve() + return w +} + +// Load loads an existing file on disk and returns a new configuration +// wrapper. +func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) { + fd, err := os.Open(path) + if err != nil { + return nil, err + } + defer fd.Close() + + cfg, err := ReadXML(fd, myID) + if err != nil { + return nil, err + } + + return Wrap(path, cfg), nil +} + +// Serve handles configuration replace events and calls any interested +// handlers. It is started automatically by Wrap() and Load() and should not +// be run manually. +func (w *ConfigWrapper) Serve() { + for cfg := range w.replaces { + w.mut.Lock() + subs := w.subs + w.mut.Unlock() + for _, h := range subs { + h.Changed(cfg) + } + } +} + +// Stop stops the Serve() loop. +func (w *ConfigWrapper) Stop() { + close(w.replaces) +} + +// Subscriber registers the given handler to be called on any future +// configuration changes. +func (w *ConfigWrapper) Subscribe(h Handler) { + w.mut.Lock() + w.subs = append(w.subs, h) + w.mut.Unlock() +} + +// Raw returns the currently wrapped Configuration object. +func (w *ConfigWrapper) Raw() Configuration { + return w.cfg +} + +// Replace swaps the current configuration object for the given one. +func (w *ConfigWrapper) Replace(cfg Configuration) { + w.mut.Lock() + defer w.mut.Unlock() + + w.cfg = cfg + w.deviceMap = nil + w.folderMap = nil + w.replaces <- cfg +} + +// Devices returns a map of devices. Device structures should not be changed, +// other than for the purpose of updating via SetDevice(). +func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration { + w.mut.Lock() + defer w.mut.Unlock() + if w.deviceMap == nil { + w.deviceMap = make(map[protocol.DeviceID]DeviceConfiguration, len(w.cfg.Devices)) + for _, dev := range w.cfg.Devices { + w.deviceMap[dev.DeviceID] = dev + } + } + return w.deviceMap +} + +// SetDevice adds a new device to the configuration, or overwrites an existing +// device with the same ID. +func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) { + w.mut.Lock() + defer w.mut.Unlock() + + w.deviceMap = nil + + for i := range w.cfg.Devices { + if w.cfg.Devices[i].DeviceID == dev.DeviceID { + w.cfg.Devices[i] = dev + w.replaces <- w.cfg + return + } + } + + w.cfg.Devices = append(w.cfg.Devices, dev) + w.replaces <- w.cfg +} + +// Devices returns a map of folders. Folder structures should not be changed, +// other than for the purpose of updating via SetFolder(). +func (w *ConfigWrapper) Folders() map[string]FolderConfiguration { + w.mut.Lock() + defer w.mut.Unlock() + if w.folderMap == nil { + w.folderMap = make(map[string]FolderConfiguration, len(w.cfg.Folders)) + for _, fld := range w.cfg.Folders { + w.folderMap[fld.ID] = fld + } + } + return w.folderMap +} + +// SetFolder adds a new folder to the configuration, or overwrites an existing +// folder with the same ID. +func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) { + w.mut.Lock() + defer w.mut.Unlock() + + w.folderMap = nil + + for i := range w.cfg.Folders { + if w.cfg.Folders[i].ID == fld.ID { + w.cfg.Folders[i] = fld + w.replaces <- w.cfg + return + } + } + + w.cfg.Folders = append(w.cfg.Folders, fld) + w.replaces <- w.cfg +} + +// Options returns the current options configuration object. +func (w *ConfigWrapper) Options() OptionsConfiguration { + w.mut.Lock() + defer w.mut.Unlock() + return w.cfg.Options +} + +// SetOptions replaces the current options configuration object. +func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) { + w.mut.Lock() + defer w.mut.Unlock() + w.cfg.Options = opts + w.replaces <- w.cfg +} + +// GUI returns the current GUI configuration object. +func (w *ConfigWrapper) GUI() GUIConfiguration { + w.mut.Lock() + defer w.mut.Unlock() + return w.cfg.GUI +} + +// SetGUI replaces the current GUI configuration object. +func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) { + w.mut.Lock() + defer w.mut.Unlock() + w.cfg.GUI = gui + w.replaces <- w.cfg +} + +// InvalidateFolder sets the invalid marker on the given folder. +func (w *ConfigWrapper) InvalidateFolder(id string, err string) { + w.mut.Lock() + defer w.mut.Unlock() + + w.folderMap = nil + + for i := range w.cfg.Folders { + if w.cfg.Folders[i].ID == id { + w.cfg.Folders[i].Invalid = err + w.replaces <- w.cfg + return + } + } +} + +// Save writes the configuration to disk, and generates a ConfigSaved event. +func (w *ConfigWrapper) Save() error { + fd, err := ioutil.TempFile(filepath.Dir(w.path), "cfg") + if err != nil { + return err + } + + err = w.cfg.WriteXML(fd) + if err != nil { + fd.Close() + return err + } + + err = fd.Close() + if err != nil { + return err + } + + events.Default.Log(events.ConfigSaved, w.cfg) + + return osutil.Rename(fd.Name(), w.path) +} diff --git a/internal/model/model.go b/internal/model/model.go index 41003442..28e47ad4 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -81,7 +81,7 @@ type service interface { } type Model struct { - cfg *config.Configuration + cfg *config.ConfigWrapper db *leveldb.DB deviceName string @@ -118,7 +118,7 @@ var ( // NewModel creates and starts a new model. The model starts in read-only mode, // where it sends index information to connected peers and responds to requests // for file data without altering the local folder in any way. -func NewModel(cfg *config.Configuration, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model { +func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model { m := &Model{ cfg: cfg, db: db, @@ -257,8 +257,8 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo { // Returns statistics about each device func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics { var res = make(map[string]stats.DeviceStatistics) - for _, device := range m.cfg.Devices { - res[device.DeviceID.String()] = m.deviceStatRef(device.DeviceID).GetStatistics() + for id := range m.cfg.Devices() { + res[id.String()] = m.deviceStatRef(id).GetStatistics() } return res } @@ -505,14 +505,14 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon if name := cm.GetOption("name"); name != "" { l.Infof("Device %s name is %q", deviceID, name) - device := m.cfg.GetDeviceConfiguration(deviceID) - if device != nil && device.Name == "" { + device, ok := m.cfg.Devices()[deviceID] + if ok && device.Name == "" { device.Name = name - m.cfg.Save() + m.cfg.SetDevice(device) } } - if m.cfg.GetDeviceConfiguration(deviceID).Introducer { + if m.cfg.Devices()[deviceID].Introducer { // This device is an introducer. Go through the announced lists of folders // and devices and add what we are missing. @@ -530,12 +530,13 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon var id protocol.DeviceID copy(id[:], device.ID) - if m.cfg.GetDeviceConfiguration(id) == nil { + if _, ok := m.cfg.Devices()[id]; !ok { // The device is currently unknown. Add it to the config. l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID) newDeviceCfg := config.DeviceConfiguration{ - DeviceID: id, + DeviceID: id, + Compression: true, } // The introducers' introducers are also our introducers. @@ -544,8 +545,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon newDeviceCfg.Introducer = true } - m.cfg.Devices = append(m.cfg.Devices, newDeviceCfg) - + m.cfg.SetDevice(newDeviceCfg) changed = true } @@ -565,10 +565,11 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon m.deviceFolders[id] = append(m.deviceFolders[id], folder.ID) m.folderDevices[folder.ID] = append(m.folderDevices[folder.ID], id) - folderCfg := m.cfg.GetFolderConfiguration(folder.ID) + folderCfg := m.cfg.Folders()[folder.ID] folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{ DeviceID: id, }) + m.cfg.SetFolder(folderCfg) changed = true } @@ -973,7 +974,7 @@ func (m *Model) ScanFolders() { go func() { err := m.ScanFolder(folder) if err != nil { - invalidateFolder(m.cfg, folder, err) + m.cfg.InvalidateFolder(folder, err.Error()) } wg.Done() }() @@ -1131,7 +1132,7 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe ID: device[:], Flags: protocol.FlagShareTrusted, } - if deviceCfg := m.cfg.GetDeviceConfiguration(device); deviceCfg.Introducer { + if deviceCfg := m.cfg.Devices()[device]; deviceCfg.Introducer { cn.Flags |= protocol.FlagIntroducer } cr.Devices = append(cr.Devices, cn) diff --git a/internal/model/model_test.go b/internal/model/model_test.go index 85852806..36b276b2 100644 --- a/internal/model/model_test.go +++ b/internal/model/model_test.go @@ -68,7 +68,7 @@ func init() { func TestRequest(t *testing.T) { db, _ := leveldb.Open(storage.NewMemStorage(), nil) - m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db) + m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db) m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"}) m.ScanFolder("default") @@ -258,7 +258,7 @@ func TestDeviceRename(t *testing.T) { ClientVersion: "v0.9.4", } - cfg := config.New("/tmp/test", device1) + cfg := config.New(device1) cfg.Devices = []config.DeviceConfiguration{ { DeviceID: device1, @@ -266,7 +266,7 @@ func TestDeviceRename(t *testing.T) { } db, _ := leveldb.Open(storage.NewMemStorage(), nil) - m := NewModel(&cfg, "device", "syncthing", "dev", db) + m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db) if cfg.Devices[0].Name != "" { t.Errorf("Device already has a name") } @@ -295,7 +295,7 @@ func TestDeviceRename(t *testing.T) { } func TestClusterConfig(t *testing.T) { - cfg := config.New("/tmp/test", device1) + cfg := config.New(device1) cfg.Devices = []config.DeviceConfiguration{ { DeviceID: device1, @@ -324,7 +324,7 @@ func TestClusterConfig(t *testing.T) { db, _ := leveldb.Open(storage.NewMemStorage(), nil) - m := NewModel(&cfg, "device", "syncthing", "dev", db) + m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db) m.AddFolder(cfg.Folders[0]) m.AddFolder(cfg.Folders[1]) diff --git a/internal/model/puller.go b/internal/model/puller.go index e2ffd8e4..3656dd91 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -184,7 +184,7 @@ loop: } p.model.setState(p.folder, FolderScanning) if err := p.model.ScanFolder(p.folder); err != nil { - invalidateFolder(p.model.cfg, p.folder, err) + p.model.cfg.InvalidateFolder(p.folder, err.Error()) break loop } p.model.setState(p.folder, FolderIdle) @@ -687,7 +687,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { // clean deletes orphaned temporary files func (p *Puller) clean() { - keep := time.Duration(p.model.cfg.Options.KeepTemporariesH) * time.Hour + keep := time.Duration(p.model.cfg.Options().KeepTemporariesH) * time.Hour now := time.Now() filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error { if err != nil { diff --git a/internal/model/puller_test.go b/internal/model/puller_test.go index 8c8a13fb..b878e6da 100644 --- a/internal/model/puller_test.go +++ b/internal/model/puller_test.go @@ -63,7 +63,7 @@ func TestHandleFile(t *testing.T) { requiredFile.Blocks = blocks[1:] db, _ := leveldb.Open(storage.NewMemStorage(), nil) - m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db) + m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db) m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"}) m.updateLocal("default", existingFile) @@ -130,7 +130,7 @@ func TestHandleFileWithTemp(t *testing.T) { requiredFile.Blocks = blocks[1:] db, _ := leveldb.Open(storage.NewMemStorage(), nil) - m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db) + m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db) m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"}) m.updateLocal("default", existingFile) diff --git a/internal/model/scanner.go b/internal/model/scanner.go index d85c9307..b736fb4c 100644 --- a/internal/model/scanner.go +++ b/internal/model/scanner.go @@ -49,7 +49,7 @@ func (s *Scanner) Serve() { s.model.setState(s.folder, FolderScanning) if err := s.model.ScanFolder(s.folder); err != nil { - invalidateFolder(s.model.cfg, s.folder, err) + s.model.cfg.InvalidateFolder(s.folder, err.Error()) return } s.model.setState(s.folder, FolderIdle) diff --git a/internal/osutil/osutil.go b/internal/osutil/osutil.go index 72a5d311..a760a9e8 100644 --- a/internal/osutil/osutil.go +++ b/internal/osutil/osutil.go @@ -17,12 +17,17 @@ package osutil import ( + "errors" + "fmt" "os" "path/filepath" "runtime" + "strings" "sync" ) +var ErrNoHome = errors.New("No home directory found - set $HOME (or the platform equivalent).") + // Try to keep this entire operation atomic-like. We shouldn't be doing this // often enough that there is any contention on this lock. var renameLock sync.Mutex @@ -81,3 +86,40 @@ func InWritableDir(fn func(string) error, path string) error { return fn(path) } + +func ExpandTilde(path string) (string, error) { + if path == "~" { + return getHomeDir() + } + + path = filepath.FromSlash(path) + if !strings.HasPrefix(path, fmt.Sprintf("~%c", os.PathSeparator)) { + return path, nil + } + + home, err := getHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, path[2:]), nil +} + +func getHomeDir() (string, error) { + var home string + + switch runtime.GOOS { + case "windows": + home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath")) + if home == "" { + home = os.Getenv("UserProfile") + } + default: + home = os.Getenv("HOME") + } + + if home == "" { + return "", ErrNoHome + } + + return home, nil +}