Use a configuration wrapper to handle loads and saves

This commit is contained in:
Jakob Borg 2014-10-06 09:25:45 +02:00
parent d476c2b613
commit 9b11609b63
12 changed files with 630 additions and 398 deletions

View File

@ -37,6 +37,7 @@ import (
"github.com/syncthing/syncthing/internal/events" "github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/logger" "github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/model" "github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol" "github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/upgrade" "github.com/syncthing/syncthing/internal/upgrade"
"github.com/vitrun/qart/qr" "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 folder = qs.Get("folder")
var res = make(map[string]interface{}) var res = make(map[string]interface{})
for _, cr := range cfg.Folders { res["invalid"] = cfg.Folders()[folder].Invalid
if cr.ID == folder {
res["invalid"] = cr.Invalid
break
}
}
globalFiles, globalDeleted, globalBytes := m.GlobalSize(folder) globalFiles, globalDeleted, globalBytes := m.GlobalSize(folder)
res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes 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) { func restGetConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") 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) { 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) http.Error(w, err.Error(), 500)
return return
} else { } else {
if newCfg.GUI.Password != cfg.GUI.Password { if newCfg.GUI.Password != cfg.GUI().Password {
if newCfg.GUI.Password != "" { if newCfg.GUI.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0) hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
if err != nil { 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 // 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 // UR was enabled
newCfg.Options.URAccepted = usageReportVersion newCfg.Options.URAccepted = usageReportVersion
err := sendUsageReport(m) err := sendUsageReport(m)
@ -342,7 +338,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
l.Infoln("Usage report:", err) l.Infoln("Usage report:", err)
} }
go usageReportingLoop(m) go usageReportingLoop(m)
} else if newCfg.Options.URAccepted < cfg.Options.URAccepted { } else if newCfg.Options.URAccepted < curAcc {
// UR was disabled // UR was disabled
newCfg.Options.URAccepted = -1 newCfg.Options.URAccepted = -1
stopUsageReporting() stopUsageReporting()
@ -350,10 +346,8 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
// Activate and save // Activate and save
configInSync = !config.ChangeRequiresRestart(cfg, newCfg) cfg.Replace(newCfg)
newCfg.Location = cfg.Location cfg.Save()
newCfg.Save()
cfg = newCfg
} }
} }
@ -391,13 +385,14 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
var m runtime.MemStats var m runtime.MemStats
runtime.ReadMemStats(&m) runtime.ReadMemStats(&m)
tilde, _ := osutil.ExpandTilde("~")
res := make(map[string]interface{}) res := make(map[string]interface{})
res["myID"] = myID.String() res["myID"] = myID.String()
res["goroutines"] = runtime.NumGoroutine() res["goroutines"] = runtime.NumGoroutine()
res["alloc"] = m.Alloc res["alloc"] = m.Alloc
res["sys"] = m.Sys - m.HeapReleased res["sys"] = m.Sys - m.HeapReleased
res["tilde"] = expandTilde("~") res["tilde"] = tilde
if cfg.Options.GlobalAnnEnabled && discoverer != nil { if cfg.Options().GlobalAnnEnabled && discoverer != nil {
res["extAnnounceOK"] = discoverer.ExtAnnounceOK() res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
} }
cpuUsageLock.RLock() cpuUsageLock.RLock()
@ -606,7 +601,7 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques
tot := map[string]float64{} tot := map[string]float64{}
count := map[string]float64{} count := map[string]float64{}
for _, folder := range cfg.Folders { for _, folder := range cfg.Folders() {
for _, device := range folder.DeviceIDs() { for _, device := range folder.DeviceIDs() {
deviceStr := device.String() deviceStr := device.String()
if m.ConnectedTo(device) { if m.ConnectedTo(device) {

View File

@ -26,6 +26,7 @@ import (
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -44,6 +45,7 @@ import (
"github.com/syncthing/syncthing/internal/files" "github.com/syncthing/syncthing/internal/files"
"github.com/syncthing/syncthing/internal/logger" "github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/model" "github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol" "github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/upgrade" "github.com/syncthing/syncthing/internal/upgrade"
"github.com/syncthing/syncthing/internal/upnp" "github.com/syncthing/syncthing/internal/upnp"
@ -94,7 +96,7 @@ func init() {
} }
var ( var (
cfg config.Configuration cfg *config.ConfigWrapper
myID protocol.DeviceID myID protocol.DeviceID
confDir string confDir string
logFlags int = log.Ltime logFlags int = log.Ltime
@ -184,7 +186,11 @@ var (
) )
func main() { 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(&reset, "reset", false, "Prepare to resync from cluster")
flag.BoolVar(&showVersion, "version", false, "Show version") flag.BoolVar(&showVersion, "version", false, "Show version")
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade") flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
@ -206,7 +212,10 @@ func main() {
l.SetFlags(logFlags) l.SetFlags(logFlags)
if generateDir != "" { if generateDir != "" {
dir := expandTilde(generateDir) dir, err := osutil.ExpandTilde(generateDir)
if err != nil {
l.Fatalln("generate:", err)
}
info, err := os.Stat(dir) info, err := os.Stat(dir)
if err != nil { if err != nil {
@ -234,7 +243,10 @@ func main() {
return 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() { if info, err := os.Stat(confDir); err == nil && !info.IsDir() {
l.Fatalln("Config directory", confDir, "is not a directory") 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}) 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. // Ensure that that we have a certificate and key.
cert, err = loadCert(confDir, "") cert, err = loadCert(confDir, "")
if err != nil { if err != nil {
@ -347,8 +338,8 @@ func syncthingMain() {
cfg, err = config.Load(cfgFile, myID) cfg, err = config.Load(cfgFile, myID)
if err == nil { if err == nil {
myCfg := cfg.GetDeviceConfiguration(myID) myCfg := cfg.Devices()[myID]
if myCfg == nil || myCfg.Name == "" { if myCfg.Name == "" {
myName, _ = os.Hostname() myName, _ = os.Hostname()
} else { } else {
myName = myCfg.Name myName = myCfg.Name
@ -356,10 +347,13 @@ func syncthingMain() {
} else { } else {
l.Infoln("No config file; starting with empty defaults") l.Infoln("No config file; starting with empty defaults")
myName, _ = os.Hostname() 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) newCfg := config.New(myID)
cfg.Folders = []config.FolderConfiguration{ newCfg.Folders = []config.FolderConfiguration{
{ {
ID: "default", ID: "default",
Path: defaultFolder, Path: defaultFolder,
@ -367,7 +361,7 @@ func syncthingMain() {
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}}, Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
}, },
} }
cfg.Devices = []config.DeviceConfiguration{ newCfg.Devices = []config.DeviceConfiguration{
{ {
DeviceID: myID, DeviceID: myID,
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
@ -379,14 +373,15 @@ func syncthingMain() {
if err != nil { if err != nil {
l.Fatalln("get free port (GUI):", err) 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) port, err = getFreePort("0.0.0.0", 22000)
if err != nil { if err != nil {
l.Fatalln("get free port (BEP):", err) 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() cfg.Save()
l.Infof("Edit %s to taste or use the GUI\n", cfgFile) 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. // 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. // This will be used on connections created in the connect and listen routines.
if cfg.Options.MaxSendKbps > 0 { opts := cfg.Options()
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
if opts.MaxSendKbps > 0 {
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps))
} }
if cfg.Options.MaxRecvKbps > 0 { if opts.MaxRecvKbps > 0 {
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxRecvKbps), int64(5*1000*cfg.Options.MaxRecvKbps)) 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. // 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 // Remove database entries for folders that no longer exist in the config
folderMap := cfg.FolderMap() folders := cfg.Folders()
for _, folder := range files.ListFolders(db) { 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) l.Infof("Cleaning data for dropped folder %q", folder)
files.DropFolder(db, folder) files.DropFolder(db, folder)
} }
} }
m := model.NewModel(&cfg, myName, "syncthing", Version, db) m := model.NewModel(cfg, myName, "syncthing", Version, db)
nextFolder: nextFolder:
for i, folder := range cfg.Folders { for id, folder := range cfg.Folders() {
if folder.Invalid != "" { if folder.Invalid != "" {
continue continue
} }
folder.Path = expandTilde(folder.Path) folder.Path, err = osutil.ExpandTilde(folder.Path)
if err != nil {
l.Fatalln("home:", err)
}
m.AddFolder(folder) m.AddFolder(folder)
fi, err := os.Stat(folder.Path) 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 // Safety check. If the cached index contains files but the
// folder doesn't exist, we have a problem. We would assume // folder doesn't exist, we have a problem. We would assume
// that all files have been deleted which might not be the case, // that all files have been deleted which might not be the case,
// so mark it as invalid instead. // so mark it as invalid instead.
if err != nil || !fi.IsDir() { if err != nil || !fi.IsDir() {
l.Warnf("Stopping folder %q - path does not exist, but has files in index", folder.ID) 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 continue nextFolder
} }
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
@ -473,14 +473,14 @@ nextFolder:
// If there was another error or we could not create the // If there was another error or we could not create the
// path, the folder is invalid. // path, the folder is invalid.
l.Warnf("Stopping folder %q - %v", err) l.Warnf("Stopping folder %q - %v", err)
cfg.Folders[i].Invalid = err.Error() cfg.InvalidateFolder(id, err.Error())
continue nextFolder continue nextFolder
} }
} }
// GUI // GUI
guiCfg := overrideGUIConfig(cfg.GUI, guiAddress, guiAuthentication, guiAPIKey) guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
if guiCfg.Enabled && guiCfg.Address != "" { if guiCfg.Enabled && guiCfg.Address != "" {
addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address) addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address)
@ -511,7 +511,7 @@ nextFolder:
if err != nil { if err != nil {
l.Fatalln("Cannot start GUI:", err) 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))) urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port)))
openURL(urlOpen) openURL(urlOpen)
} }
@ -521,7 +521,7 @@ nextFolder:
// Clear out old indexes for other devices. Otherwise we'll start up and // 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 // start needing a bunch of files which are nowhere to be found. This
// needs to be changed when we correctly do persistent indexes. // needs to be changed when we correctly do persistent indexes.
for _, folderCfg := range cfg.Folders { for _, folderCfg := range cfg.Folders() {
if folderCfg.Invalid != "" { if folderCfg.Invalid != "" {
continue continue
} }
@ -536,8 +536,11 @@ nextFolder:
// Remove all .idx* files that don't belong to an active folder. // Remove all .idx* files that don't belong to an active folder.
validIndexes := make(map[string]bool) validIndexes := make(map[string]bool)
for _, folder := range cfg.Folders { for _, folder := range cfg.Folders() {
dir := expandTilde(folder.Path) dir, err := osutil.ExpandTilde(folder.Path)
if err != nil {
l.Fatalln("home:", err)
}
id := fmt.Sprintf("%x", sha1.Sum([]byte(dir))) id := fmt.Sprintf("%x", sha1.Sum([]byte(dir)))
validIndexes[id] = true validIndexes[id] = true
} }
@ -558,7 +561,7 @@ nextFolder:
// The default port we announce, possibly modified by setupUPnP next. // 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 { if err != nil {
l.Fatalln("Bad listen address:", err) l.Fatalln("Bad listen address:", err)
} }
@ -566,7 +569,7 @@ nextFolder:
// UPnP // UPnP
if cfg.Options.UPnPEnabled { if opts.UPnPEnabled {
setupUPnP() setupUPnP()
} }
@ -574,7 +577,7 @@ nextFolder:
discoverer = discovery(externalPort) discoverer = discovery(externalPort)
go listenConnect(myID, m, tlsCfg) go listenConnect(myID, m, tlsCfg)
for _, folder := range cfg.Folders { for _, folder := range cfg.Folders() {
if folder.Invalid != "" { if folder.Invalid != "" {
continue continue
} }
@ -599,17 +602,18 @@ nextFolder:
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
for _, device := range cfg.Devices { for _, device := range cfg.Devices() {
if len(device.Name) > 0 { if len(device.Name) > 0 {
l.Infof("Device %s is %q at %v", device.DeviceID, device.Name, device.Addresses) 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") 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 usageReportingLoop(m)
go func() { go func() {
time.Sleep(10 * time.Minute) time.Sleep(10 * time.Minute)
@ -620,11 +624,11 @@ nextFolder:
}() }()
} }
if cfg.Options.RestartOnWakeup { if opts.RestartOnWakeup {
go standbyMonitor() go standbyMonitor()
} }
if cfg.Options.AutoUpgradeIntervalH > 0 { if opts.AutoUpgradeIntervalH > 0 {
go autoUpgrade() go autoUpgrade()
} }
@ -645,8 +649,8 @@ func generateEvents() {
} }
func setupUPnP() { func setupUPnP() {
if len(cfg.Options.ListenAddress) == 1 { if opts := cfg.Options(); len(opts.ListenAddress) == 1 {
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0]) _, portStr, err := net.SplitHostPort(opts.ListenAddress[0])
if err != nil { if err != nil {
l.Warnln("Bad listen address:", err) l.Warnln("Bad listen address:", err)
} else { } else {
@ -666,7 +670,7 @@ func setupUPnP() {
l.Debugf("UPnP: %v", err) l.Debugf("UPnP: %v", err)
} }
} }
if cfg.Options.UPnPRenewal > 0 { if opts.UPnPRenewal > 0 {
go renewUPnP(port) go renewUPnP(port)
} }
} }
@ -681,7 +685,7 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
rnd := rand.NewSource(certSeed(cert.Certificate[0])) rnd := rand.NewSource(certSeed(cert.Certificate[0]))
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
r := 1024 + int(rnd.Int63()%(65535-1024)) 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 { if err == nil {
return r return r
} }
@ -691,7 +695,8 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
func renewUPnP(port int) { func renewUPnP(port int) {
for { 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() igd, err := upnp.Discover()
if err != nil { if err != nil {
@ -700,7 +705,7 @@ func renewUPnP(port int) {
// Just renew the same port that we already have // Just renew the same port that we already have
if externalPort != 0 { 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 { if err == nil {
l.Infoln("Renewed UPnP port mapping - external port", externalPort) l.Infoln("Renewed UPnP port mapping - external port", externalPort)
continue continue
@ -715,7 +720,7 @@ func renewUPnP(port int) {
externalPort = r externalPort = r
l.Infoln("Updated UPnP port mapping - external port", externalPort) l.Infoln("Updated UPnP port mapping - external port", externalPort)
discoverer.StopGlobal() discoverer.StopGlobal()
discoverer.StartGlobal(cfg.Options.GlobalAnnServer, uint16(r)) discoverer.StartGlobal(opts.GlobalAnnServer, uint16(r))
continue continue
} }
l.Warnln("Failed to update UPnP port mapping - external port", externalPort) l.Warnln("Failed to update UPnP port mapping - external port", externalPort)
@ -724,7 +729,7 @@ func renewUPnP(port int) {
func resetFolders() { func resetFolders() {
suffix := fmt.Sprintf(".syncthing-reset-%d", time.Now().UnixNano()) 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 { if _, err := os.Stat(folder.Path); err == nil {
l.Infof("Reset: Moving %s -> %s", folder.Path, folder.Path+suffix) l.Infof("Reset: Moving %s -> %s", folder.Path, folder.Path+suffix)
os.Rename(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) var conns = make(chan *tls.Conn)
// Listen // Listen
for _, addr := range cfg.Options.ListenAddress { for _, addr := range cfg.Options().ListenAddress {
go listenTLS(conns, addr, tlsCfg) go listenTLS(conns, addr, tlsCfg)
} }
@ -815,8 +820,8 @@ next:
continue continue
} }
for _, deviceCfg := range cfg.Devices { for deviceID, deviceCfg := range cfg.Devices() {
if deviceCfg.DeviceID == remoteID { if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to // Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced // "syncthing" when generating, but the user may have replaced
// the certificate and used another name. // 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 var delay time.Duration = 1 * time.Second
for { for {
nextDevice: nextDevice:
for _, deviceCfg := range cfg.Devices { for deviceID, deviceCfg := range cfg.Devices() {
if deviceCfg.DeviceID == myID { if deviceID == myID {
continue continue
} }
if m.ConnectedTo(deviceCfg.DeviceID) { if m.ConnectedTo(deviceID) {
continue continue
} }
@ -930,7 +935,7 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
for _, addr := range deviceCfg.Addresses { for _, addr := range deviceCfg.Addresses {
if addr == "dynamic" { if addr == "dynamic" {
if discoverer != nil { if discoverer != nil {
t := discoverer.Lookup(deviceCfg.DeviceID) t := discoverer.Lookup(deviceID)
if len(t) == 0 { if len(t) == 0 {
continue continue
} }
@ -987,7 +992,7 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
time.Sleep(delay) time.Sleep(delay)
delay *= 2 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 delay = maxD
} }
} }
@ -1010,16 +1015,17 @@ func setTCPOptions(conn *net.TCPConn) {
} }
func discovery(extPort int) *discover.Discoverer { 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") 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") l.Infoln("Starting global discovery announcements")
disc.StartGlobal(cfg.Options.GlobalAnnServer, uint16(extPort)) disc.StartGlobal(opts.GlobalAnnServer, uint16(extPort))
} }
return disc return disc
@ -1041,56 +1047,23 @@ func ensureDir(dir string, mode int) {
} }
} }
func getDefaultConfDir() string { func getDefaultConfDir() (string, error) {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
return filepath.Join(os.Getenv("LocalAppData"), "Syncthing") return filepath.Join(os.Getenv("LocalAppData"), "Syncthing"), nil
case "darwin": case "darwin":
return expandTilde("~/Library/Application Support/Syncthing") return osutil.ExpandTilde("~/Library/Application Support/Syncthing")
default: default:
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" { if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
return filepath.Join(xdgCfg, "syncthing") return filepath.Join(xdgCfg, "syncthing"), nil
} else { } 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 // 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, // tried in succession and the first to succeed is returned. If none succeed,
// a random high port is returned. // a random high port is returned.
@ -1112,10 +1085,7 @@ func getFreePort(host string, ports ...int) (int, error) {
return addr.Port, nil return addr.Port, nil
} }
func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration { func overrideGUIConfig(cfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration {
// Make a copy of the config
cfg := originalCfg
if address == "" { if address == "" {
address = os.Getenv("STGUIADDRESS") address = os.Getenv("STGUIADDRESS")
} }
@ -1123,16 +1093,25 @@ func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authenticat
if address != "" { if address != "" {
cfg.Enabled = true cfg.Enabled = true
addressParts := strings.SplitN(address, "://", 2) if !strings.Contains(address, "//") {
switch addressParts[0] { // 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": case "http":
cfg.UseTLS = false cfg.UseTLS = false
case "https": case "https":
cfg.UseTLS = true cfg.UseTLS = true
default: default:
l.Fatalln("Unidentified protocol", addressParts[0]) l.Fatalln("Unknown scheme:", parsed.Scheme)
}
cfg.Address = parsed.Host
} }
cfg.Address = addressParts[1]
} }
if authentication == "" { if authentication == "" {
@ -1183,7 +1162,7 @@ func standbyMonitor() {
func autoUpgrade() { func autoUpgrade() {
var skipped bool var skipped bool
interval := time.Duration(cfg.Options.AutoUpgradeIntervalH) * time.Hour interval := time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour
for { for {
if skipped { if skipped {
time.Sleep(interval) time.Sleep(interval)

View File

@ -42,13 +42,13 @@ func reportData(m *model.Model) map[string]interface{} {
res["version"] = Version res["version"] = Version
res["longVersion"] = LongVersion res["longVersion"] = LongVersion
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
res["numFolders"] = len(cfg.Folders) res["numFolders"] = len(cfg.Folders())
res["numDevices"] = len(cfg.Devices) res["numDevices"] = len(cfg.Devices())
var totFiles, maxFiles int var totFiles, maxFiles int
var totBytes, maxBytes int64 var totBytes, maxBytes int64
for _, folder := range cfg.Folders { for folderID := range cfg.Folders() {
files, _, bytes := m.GlobalSize(folder.ID) files, _, bytes := m.GlobalSize(folderID)
totFiles += files totFiles += files
totBytes += bytes totBytes += bytes
if files > maxFiles { if files > maxFiles {

View File

@ -19,22 +19,20 @@ package config
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"os" "os"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"code.google.com/p/go.crypto/bcrypt" "code.google.com/p/go.crypto/bcrypt"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/logger" "github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol" "github.com/syncthing/syncthing/internal/protocol"
) )
var l = logger.DefaultLogger var l = logger.DefaultLogger
type Configuration struct { type Configuration struct {
Location string `xml:"-" json:"-"`
Version int `xml:"version,attr" default:"5"` Version int `xml:"version,attr" default:"5"`
Folders []FolderConfiguration `xml:"folder"` Folders []FolderConfiguration `xml:"folder"`
Devices []DeviceConfiguration `xml:"device"` Devices []DeviceConfiguration `xml:"device"`
@ -62,6 +60,15 @@ type FolderConfiguration struct {
Deprecated_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"` 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 VersioningConfiguration struct {
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
Params map[string]string Params map[string]string
@ -103,15 +110,6 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl
return nil 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 { type DeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr"` DeviceID protocol.DeviceID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"` Name string `xml:"name,attr,omitempty"`
@ -164,6 +162,42 @@ type GUIConfiguration struct {
APIKey string `xml:"apikey,omitempty"` 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 { func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices)) m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
for _, n := range cfg.Devices { for _, n := range cfg.Devices {
@ -198,117 +232,6 @@ func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
return m 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) { func (cfg *Configuration) prepare(myID protocol.DeviceID) {
fillNilSlices(&cfg.Options) fillNilSlices(&cfg.Options)
@ -356,7 +279,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
cfg.Options.Deprecated_URDeclined = false cfg.Options.Deprecated_URDeclined = false
cfg.Options.Deprecated_UREnabled = false cfg.Options.Deprecated_UREnabled = false
// Upgrade to v2 configuration if appropriate // Upgrade to v1 configuration if appropriate
if cfg.Version == 1 { if cfg.Version == 1 {
convertV1V2(cfg) 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 // ChangeRequiresRestart returns true if updating the configuration requires a
// complete restart. // complete restart.
func ChangeRequiresRestart(from, to Configuration) bool { func ChangeRequiresRestart(from, to Configuration) bool {
@ -593,29 +481,79 @@ func convertV1V2(cfg *Configuration) {
cfg.Version = 2 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 { v := tag.Get("default")
return l[a].DeviceID.Compare(l[b].DeviceID) == -1 if len(v) > 0 {
} switch f.Interface().(type) {
func (l DeviceConfigurationList) Swap(a, b int) { case string:
l[a], l[b] = l[b], l[a] f.SetString(v)
}
func (l DeviceConfigurationList) Len() int { case int:
return len(l) 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 { for i := 0; i < s.NumField(); i++ {
return l[a].DeviceID.Compare(l[b].DeviceID) == -1 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 uniqueStrings(ss []string) []string {
} var m = make(map[string]bool, len(ss))
func (l FolderDeviceConfigurationList) Len() int { for _, s := range ss {
return len(l) 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 { func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
@ -664,3 +602,27 @@ loop:
} }
return devices[0:count] 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)
}

View File

@ -52,7 +52,7 @@ func TestDefaultValues(t *testing.T) {
KeepTemporariesH: 24, KeepTemporariesH: 24,
} }
cfg := New("test", device1) cfg := New(device1)
if !reflect.DeepEqual(cfg.Options, expected) { if !reflect.DeepEqual(cfg.Options, expected) {
t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options) 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) { func TestDeviceConfig(t *testing.T) {
for i, ver := range []string{"v1", "v2", "v3", "v4", "v5"} { for i, ver := range []string{"v3", "v4", "v5"} {
cfg, err := Load("testdata/"+ver+".xml", device1) wr, err := Load("testdata/"+ver+".xml", device1)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
} }
cfg := wr.cfg
expectedFolders := []FolderConfiguration{ expectedFolders := []FolderConfiguration{
{ {
@ -120,8 +121,9 @@ func TestNoListenAddress(t *testing.T) {
} }
expected := []string{""} expected := []string{""}
if !reflect.DeepEqual(cfg.Options.ListenAddress, expected) { actual := cfg.Options().ListenAddress
t.Errorf("Unexpected ListenAddress %#v", 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) 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) t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
} }
} }
func TestDeviceAddressesDynamic(t *testing.T) { func TestDeviceAddressesDynamic(t *testing.T) {
name, _ := os.Hostname() name, _ := os.Hostname()
expected := []DeviceConfiguration{ expected := map[protocol.DeviceID]DeviceConfiguration{
{ device1: {
DeviceID: device1, DeviceID: device1,
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
Compression: true, Compression: true,
}, },
{ device2: {
DeviceID: device2, DeviceID: device2,
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
Compression: true, Compression: true,
}, },
{ device3: {
DeviceID: device3, DeviceID: device3,
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
Compression: true, Compression: true,
}, },
{ device4: {
DeviceID: device4, DeviceID: device4,
Name: name, // Set when auto created Name: name, // Set when auto created
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
@ -185,27 +187,28 @@ func TestDeviceAddressesDynamic(t *testing.T) {
t.Error(err) t.Error(err)
} }
if !reflect.DeepEqual(cfg.Devices, expected) { actual := cfg.Devices()
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, cfg.Devices) if !reflect.DeepEqual(actual, expected) {
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, actual)
} }
} }
func TestDeviceAddressesStatic(t *testing.T) { func TestDeviceAddressesStatic(t *testing.T) {
name, _ := os.Hostname() name, _ := os.Hostname()
expected := []DeviceConfiguration{ expected := map[protocol.DeviceID]DeviceConfiguration{
{ device1: {
DeviceID: device1, DeviceID: device1,
Addresses: []string{"192.0.2.1", "192.0.2.2"}, Addresses: []string{"192.0.2.1", "192.0.2.2"},
}, },
{ device2: {
DeviceID: device2, DeviceID: device2,
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"}, Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
}, },
{ device3: {
DeviceID: device3, DeviceID: device3,
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"}, Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
}, },
{ device4: {
DeviceID: device4, DeviceID: device4,
Name: name, // Set when auto created Name: name, // Set when auto created
Addresses: []string{"dynamic"}, Addresses: []string{"dynamic"},
@ -217,8 +220,9 @@ func TestDeviceAddressesStatic(t *testing.T) {
t.Error(err) t.Error(err)
} }
if !reflect.DeepEqual(cfg.Devices, expected) { actual := cfg.Devices()
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, 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) t.Error(err)
} }
vc := cfg.Folders[0].Versioning vc := cfg.Folders()["test"].Versioning
if vc.Type != "simple" { if vc.Type != "simple" {
t.Errorf(`vc.Type %q != "simple"`, vc.Type) t.Errorf(`vc.Type %q != "simple"`, vc.Type)
} }
@ -254,10 +258,11 @@ func TestNewSaveLoad(t *testing.T) {
return err == nil return err == nil
} }
cfg := New(path, device1) intCfg := New(device1)
cfg := Wrap(path, intCfg)
// To make the equality pass later // To make the equality pass later
cfg.XMLName.Local = "configuration" cfg.cfg.XMLName.Local = "configuration"
if exists(path) { if exists(path) {
t.Error(path, "exists") t.Error(path, "exists")
@ -276,20 +281,8 @@ func TestNewSaveLoad(t *testing.T) {
t.Error(err) t.Error(err)
} }
if !reflect.DeepEqual(cfg, cfg2) { if !reflect.DeepEqual(cfg.Raw(), cfg2.Raw()) {
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2) t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg.Raw(), cfg2.Raw())
}
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)
} }
os.Remove(path) os.Remove(path)

260
internal/config/wrapper.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View File

@ -81,7 +81,7 @@ type service interface {
} }
type Model struct { type Model struct {
cfg *config.Configuration cfg *config.ConfigWrapper
db *leveldb.DB db *leveldb.DB
deviceName string deviceName string
@ -118,7 +118,7 @@ var (
// NewModel creates and starts a new model. The model starts in read-only mode, // 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 // where it sends index information to connected peers and responds to requests
// for file data without altering the local folder in any way. // 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{ m := &Model{
cfg: cfg, cfg: cfg,
db: db, db: db,
@ -257,8 +257,8 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
// Returns statistics about each device // Returns statistics about each device
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics { func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
var res = make(map[string]stats.DeviceStatistics) var res = make(map[string]stats.DeviceStatistics)
for _, device := range m.cfg.Devices { for id := range m.cfg.Devices() {
res[device.DeviceID.String()] = m.deviceStatRef(device.DeviceID).GetStatistics() res[id.String()] = m.deviceStatRef(id).GetStatistics()
} }
return res return res
} }
@ -505,14 +505,14 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
if name := cm.GetOption("name"); name != "" { if name := cm.GetOption("name"); name != "" {
l.Infof("Device %s name is %q", deviceID, name) l.Infof("Device %s name is %q", deviceID, name)
device := m.cfg.GetDeviceConfiguration(deviceID) device, ok := m.cfg.Devices()[deviceID]
if device != nil && device.Name == "" { if ok && device.Name == "" {
device.Name = 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 // This device is an introducer. Go through the announced lists of folders
// and devices and add what we are missing. // 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 var id protocol.DeviceID
copy(id[:], device.ID) 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. // The device is currently unknown. Add it to the config.
l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID) l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID)
newDeviceCfg := config.DeviceConfiguration{ newDeviceCfg := config.DeviceConfiguration{
DeviceID: id, DeviceID: id,
Compression: true,
} }
// The introducers' introducers are also our introducers. // The introducers' introducers are also our introducers.
@ -544,8 +545,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
newDeviceCfg.Introducer = true newDeviceCfg.Introducer = true
} }
m.cfg.Devices = append(m.cfg.Devices, newDeviceCfg) m.cfg.SetDevice(newDeviceCfg)
changed = true 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.deviceFolders[id] = append(m.deviceFolders[id], folder.ID)
m.folderDevices[folder.ID] = append(m.folderDevices[folder.ID], 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{ folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
DeviceID: id, DeviceID: id,
}) })
m.cfg.SetFolder(folderCfg)
changed = true changed = true
} }
@ -973,7 +974,7 @@ func (m *Model) ScanFolders() {
go func() { go func() {
err := m.ScanFolder(folder) err := m.ScanFolder(folder)
if err != nil { if err != nil {
invalidateFolder(m.cfg, folder, err) m.cfg.InvalidateFolder(folder, err.Error())
} }
wg.Done() wg.Done()
}() }()
@ -1131,7 +1132,7 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe
ID: device[:], ID: device[:],
Flags: protocol.FlagShareTrusted, Flags: protocol.FlagShareTrusted,
} }
if deviceCfg := m.cfg.GetDeviceConfiguration(device); deviceCfg.Introducer { if deviceCfg := m.cfg.Devices()[device]; deviceCfg.Introducer {
cn.Flags |= protocol.FlagIntroducer cn.Flags |= protocol.FlagIntroducer
} }
cr.Devices = append(cr.Devices, cn) cr.Devices = append(cr.Devices, cn)

View File

@ -68,7 +68,7 @@ func init() {
func TestRequest(t *testing.T) { func TestRequest(t *testing.T) {
db, _ := leveldb.Open(storage.NewMemStorage(), nil) 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.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
m.ScanFolder("default") m.ScanFolder("default")
@ -258,7 +258,7 @@ func TestDeviceRename(t *testing.T) {
ClientVersion: "v0.9.4", ClientVersion: "v0.9.4",
} }
cfg := config.New("/tmp/test", device1) cfg := config.New(device1)
cfg.Devices = []config.DeviceConfiguration{ cfg.Devices = []config.DeviceConfiguration{
{ {
DeviceID: device1, DeviceID: device1,
@ -266,7 +266,7 @@ func TestDeviceRename(t *testing.T) {
} }
db, _ := leveldb.Open(storage.NewMemStorage(), nil) 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 != "" { if cfg.Devices[0].Name != "" {
t.Errorf("Device already has a name") t.Errorf("Device already has a name")
} }
@ -295,7 +295,7 @@ func TestDeviceRename(t *testing.T) {
} }
func TestClusterConfig(t *testing.T) { func TestClusterConfig(t *testing.T) {
cfg := config.New("/tmp/test", device1) cfg := config.New(device1)
cfg.Devices = []config.DeviceConfiguration{ cfg.Devices = []config.DeviceConfiguration{
{ {
DeviceID: device1, DeviceID: device1,
@ -324,7 +324,7 @@ func TestClusterConfig(t *testing.T) {
db, _ := leveldb.Open(storage.NewMemStorage(), nil) 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[0])
m.AddFolder(cfg.Folders[1]) m.AddFolder(cfg.Folders[1])

View File

@ -184,7 +184,7 @@ loop:
} }
p.model.setState(p.folder, FolderScanning) p.model.setState(p.folder, FolderScanning)
if err := p.model.ScanFolder(p.folder); err != nil { 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 break loop
} }
p.model.setState(p.folder, FolderIdle) p.model.setState(p.folder, FolderIdle)
@ -687,7 +687,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
// clean deletes orphaned temporary files // clean deletes orphaned temporary files
func (p *Puller) clean() { 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() now := time.Now()
filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error { filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {

View File

@ -63,7 +63,7 @@ func TestHandleFile(t *testing.T) {
requiredFile.Blocks = blocks[1:] requiredFile.Blocks = blocks[1:]
db, _ := leveldb.Open(storage.NewMemStorage(), nil) 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.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
m.updateLocal("default", existingFile) m.updateLocal("default", existingFile)
@ -130,7 +130,7 @@ func TestHandleFileWithTemp(t *testing.T) {
requiredFile.Blocks = blocks[1:] requiredFile.Blocks = blocks[1:]
db, _ := leveldb.Open(storage.NewMemStorage(), nil) 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.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
m.updateLocal("default", existingFile) m.updateLocal("default", existingFile)

View File

@ -49,7 +49,7 @@ func (s *Scanner) Serve() {
s.model.setState(s.folder, FolderScanning) s.model.setState(s.folder, FolderScanning)
if err := s.model.ScanFolder(s.folder); err != nil { 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 return
} }
s.model.setState(s.folder, FolderIdle) s.model.setState(s.folder, FolderIdle)

View File

@ -17,12 +17,17 @@
package osutil package osutil
import ( import (
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync" "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 // Try to keep this entire operation atomic-like. We shouldn't be doing this
// often enough that there is any contention on this lock. // often enough that there is any contention on this lock.
var renameLock sync.Mutex var renameLock sync.Mutex
@ -81,3 +86,40 @@ func InWritableDir(fn func(string) error, path string) error {
return fn(path) 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
}