lib/config: Remove "Invalid" attribute (fixes #2471)

This contains the following behavioral changes:

 - Duplicate folder IDs is now fatal during startup
 - Invalid folder flags in the ClusterConfig is fatal for the connection
   (this will go away soon with the proto changes, as we won't have any
   unknown flags any more then)
 - Empty path is a folder error reported at runtime

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3370
This commit is contained in:
Jakob Borg 2016-07-02 19:38:39 +00:00 committed by Audrius Butkevicius
parent 8e39e2889d
commit 6d357211b2
11 changed files with 156 additions and 80 deletions

View File

@ -577,7 +577,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} { func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} {
var res = make(map[string]interface{}) var res = make(map[string]interface{})
res["invalid"] = cfg.Folders()[folder].Invalid res["invalid"] = "" // Deprecated, retains external API for now
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
@ -690,7 +690,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
to, err := config.ReadJSON(r.Body, myID) to, err := config.ReadJSON(r.Body, myID)
r.Body.Close() r.Body.Close()
if err != nil { if err != nil {
l.Warnln("decoding posted config:", err) l.Warnln("Decoding posted config:", err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }

View File

@ -11,6 +11,7 @@ import (
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -613,3 +614,55 @@ func TestRandomString(t *testing.T) {
t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"])) t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
} }
} }
func TestConfigPostOK(t *testing.T) {
cfg := bytes.NewBuffer([]byte(`{
"version": 15,
"folders": [
{"id": "foo"}
]
}`))
resp, err := testConfigPost(cfg)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Error("Expected 200 OK, not", resp.Status)
}
}
func TestConfigPostDupFolder(t *testing.T) {
cfg := bytes.NewBuffer([]byte(`{
"version": 15,
"folders": [
{"id": "foo"},
{"id": "foo"}
]
}`))
resp, err := testConfigPost(cfg)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusBadRequest {
t.Error("Expected 400 Bad Request, not", resp.Status)
}
}
func testConfigPost(data io.Reader) (*http.Response, error) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
return nil, err
}
cli := &http.Client{
Timeout: time.Second,
}
req, _ := http.NewRequest("POST", baseURL+"/rest/system/config", data)
req.Header.Set("X-API-Key", testAPIKey)
return cli.Do(req)
}

View File

@ -863,7 +863,6 @@ func loadConfig() (*config.Wrapper, error) {
cfg, err := config.Load(cfgFile, myID) cfg, err := config.Load(cfgFile, myID)
if err != nil { if err != nil {
l.Infoln("Error loading config file; using defaults for now")
myName, _ := os.Hostname() myName, _ := os.Hostname()
newCfg := defaultConfig(myName) newCfg := defaultConfig(myName)
cfg = config.Wrap(cfgFile, newCfg) cfg = config.Wrap(cfgFile, newCfg)

View File

@ -10,6 +10,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
@ -81,11 +82,15 @@ func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
util.SetDefaults(&cfg.Options) util.SetDefaults(&cfg.Options)
util.SetDefaults(&cfg.GUI) util.SetDefaults(&cfg.GUI)
err := xml.NewDecoder(r).Decode(&cfg) if err := xml.NewDecoder(r).Decode(&cfg); err != nil {
return Configuration{}, err
}
cfg.OriginalVersion = cfg.Version cfg.OriginalVersion = cfg.Version
cfg.prepare(myID) if err := cfg.prepare(myID); err != nil {
return cfg, err return Configuration{}, err
}
return cfg, nil
} }
func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) { func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
@ -97,14 +102,16 @@ func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
bs, err := ioutil.ReadAll(r) bs, err := ioutil.ReadAll(r)
if err != nil { if err != nil {
return cfg, err return Configuration{}, err
} }
err = json.Unmarshal(bs, &cfg) err = json.Unmarshal(bs, &cfg)
cfg.OriginalVersion = cfg.Version cfg.OriginalVersion = cfg.Version
cfg.prepare(myID) if err := cfg.prepare(myID); err != nil {
return cfg, err return Configuration{}, err
}
return cfg, nil
} }
type Configuration struct { type Configuration struct {
@ -154,7 +161,7 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
return err return err
} }
func (cfg *Configuration) prepare(myID protocol.DeviceID) { func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
util.FillNilSlices(&cfg.Options) util.FillNilSlices(&cfg.Options)
// Initialize any empty slices // Initialize any empty slices
@ -168,19 +175,19 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
cfg.Options.AlwaysLocalNets = []string{} cfg.Options.AlwaysLocalNets = []string{}
} }
// Check for missing, bad or duplicate folder ID:s // Prepare folders and check for duplicates. Duplicates are bad and
var seenFolders = map[string]*FolderConfiguration{} // dangerous, can't currently be resolved in the GUI, and shouldn't
// happen when configured by the GUI. We return with an error in that
// situation.
seenFolders := make(map[string]struct{})
for i := range cfg.Folders { for i := range cfg.Folders {
folder := &cfg.Folders[i] folder := &cfg.Folders[i]
folder.prepare() folder.prepare()
if seen, ok := seenFolders[folder.ID]; ok { if _, ok := seenFolders[folder.ID]; ok {
l.Warnf("Multiple folders with ID %q; disabling", folder.ID) return fmt.Errorf("duplicate folder ID %q in configuration", folder.ID)
seen.Invalid = "duplicate folder ID"
folder.Invalid = "duplicate folder ID"
} else {
seenFolders[folder.ID] = folder
} }
seenFolders[folder.ID] = struct{}{}
} }
cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses) cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses)
@ -257,6 +264,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
if cfg.GUI.APIKey == "" { if cfg.GUI.APIKey == "" {
cfg.GUI.APIKey = rand.String(32) cfg.GUI.APIKey = rand.String(32)
} }
return nil
} }
func convertV14V15(cfg *Configuration) { func convertV14V15(cfg *Configuration) {

View File

@ -595,22 +595,14 @@ func TestGUIConfigURL(t *testing.T) {
} }
} }
func TestRemoveDuplicateDevicesFolders(t *testing.T) { func TestDuplicateDevices(t *testing.T) {
wrapper, err := Load("testdata/duplicates.xml", device1) // Duplicate devices should be removed
wrapper, err := Load("testdata/dupdevices.xml", device1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// All folders are loaded, but the duplicate ones are disabled.
if l := len(wrapper.Raw().Folders); l != 3 {
t.Errorf("Incorrect number of folders, %d != 3", l)
}
for i, f := range wrapper.Raw().Folders {
if f.ID == "f1" && f.Invalid == "" {
t.Errorf("Folder %d (%q) is not set invalid", i, f.ID)
}
}
if l := len(wrapper.Raw().Devices); l != 3 { if l := len(wrapper.Raw().Devices); l != 3 {
t.Errorf("Incorrect number of devices, %d != 3", l) t.Errorf("Incorrect number of devices, %d != 3", l)
} }
@ -621,6 +613,30 @@ func TestRemoveDuplicateDevicesFolders(t *testing.T) {
} }
} }
func TestDuplicateFolders(t *testing.T) {
// Duplicate folders are a loading error
_, err := Load("testdata/dupfolders.xml", device1)
if err == nil || !strings.HasPrefix(err.Error(), "duplicate folder ID") {
t.Fatal(`Expected error to mention "duplicate folder ID":`, err)
}
}
func TestEmptyFolderPaths(t *testing.T) {
// Empty folder paths are allowed at the loading stage, and should not
// get messed up by the prepare steps (e.g., become the current dir or
// get a slash added so that it becomes the root directory or similar).
wrapper, err := Load("testdata/nopath.xml", device1)
if err != nil {
t.Fatal(err)
}
folder := wrapper.Folders()["f1"]
if folder.Path() != "" {
t.Errorf("Expected %q to be empty", folder.Path())
}
}
func TestV14ListenAddressesMigration(t *testing.T) { func TestV14ListenAddressesMigration(t *testing.T) {
tcs := [][3][]string{ tcs := [][3][]string{

View File

@ -39,7 +39,6 @@ type FolderConfiguration struct {
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"` DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"` DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
cachedPath string cachedPath string
DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"` DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
@ -70,7 +69,7 @@ func (f FolderConfiguration) Path() string {
// This is intentionally not a pointer method, because things like // This is intentionally not a pointer method, because things like
// cfg.Folders["default"].Path() should be valid. // cfg.Folders["default"].Path() should be valid.
if f.cachedPath == "" { if f.cachedPath == "" && f.RawPath != "" {
l.Infoln("bug: uncached path call (should only happen in tests)") l.Infoln("bug: uncached path call (should only happen in tests)")
return f.cleanedPath() return f.cleanedPath()
} }
@ -108,31 +107,24 @@ func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
} }
func (f *FolderConfiguration) prepare() { func (f *FolderConfiguration) prepare() {
if len(f.RawPath) == 0 { if f.RawPath != "" {
f.Invalid = "no directory configured" // The reason it's done like this:
return // C: -> C:\ -> C:\ (issue that this is trying to fix)
} // C:\somedir -> C:\somedir\ -> C:\somedir
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
// This way in the tests, we get away without OS specific separators
// in the test configs.
f.RawPath = filepath.Dir(f.RawPath + string(filepath.Separator))
// The reason it's done like this: // If we're not on Windows, we want the path to end with a slash to
// C: -> C:\ -> C:\ (issue that this is trying to fix) // penetrate symlinks. On Windows, paths must not end with a slash.
// C:\somedir -> C:\somedir\ -> C:\somedir if runtime.GOOS != "windows" && f.RawPath[len(f.RawPath)-1] != filepath.Separator {
// C:\somedir\ -> C:\somedir\\ -> C:\somedir f.RawPath = f.RawPath + string(filepath.Separator)
// This way in the tests, we get away without OS specific separators }
// in the test configs.
f.RawPath = filepath.Dir(f.RawPath + string(filepath.Separator))
// If we're not on Windows, we want the path to end with a slash to
// penetrate symlinks. On Windows, paths must not end with a slash.
if runtime.GOOS != "windows" && f.RawPath[len(f.RawPath)-1] != filepath.Separator {
f.RawPath = f.RawPath + string(filepath.Separator)
} }
f.cachedPath = f.cleanedPath() f.cachedPath = f.cleanedPath()
if f.ID == "" {
f.ID = "default"
}
if f.RescanIntervalS > MaxRescanIntervalS { if f.RescanIntervalS > MaxRescanIntervalS {
f.RescanIntervalS = MaxRescanIntervalS f.RescanIntervalS = MaxRescanIntervalS
} else if f.RescanIntervalS < 0 { } else if f.RescanIntervalS < 0 {
@ -145,6 +137,10 @@ func (f *FolderConfiguration) prepare() {
} }
func (f *FolderConfiguration) cleanedPath() string { func (f *FolderConfiguration) cleanedPath() string {
if f.RawPath == "" {
return ""
}
cleaned := f.RawPath cleaned := f.RawPath
// Attempt tilde expansion; leave unchanged in case of error // Attempt tilde expansion; leave unchanged in case of error

View File

@ -15,15 +15,6 @@
<!-- duplicate, will be removed --> <!-- duplicate, will be removed -->
<address>192.0.2.5</address> <address>192.0.2.5</address>
</device> </device>
<folder id="f1" directory="testdata/">
<!-- duplicate, will be disabled -->
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>
</folder>
<folder id="f1" directory="testdata/">
<!-- duplicate, will be disabled -->
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
</folder>
<folder id="f2" directory="testdata/"> <folder id="f2" directory="testdata/">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device> <device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device> <device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>

6
lib/config/testdata/dupfolders.xml vendored Normal file
View File

@ -0,0 +1,6 @@
<configuration version="15">
<folder id="f1" directory="testdata/">
</folder>
<folder id="f1" directory="testdata/">
</folder>
</configuration>

4
lib/config/testdata/nopath.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<configuration version="15">
<folder id="f1">
</folder>
</configuration>

View File

@ -110,6 +110,7 @@ var (
// errors returned by the CheckFolderHealth method // errors returned by the CheckFolderHealth method
var ( var (
errFolderPathEmpty = errors.New("folder path empty")
errFolderPathMissing = errors.New("folder path missing") errFolderPathMissing = errors.New("folder path missing")
errFolderMarkerMissing = errors.New("folder marker missing") errFolderMarkerMissing = errors.New("folder marker missing")
errHomeDiskNoSpace = errors.New("home disk has insufficient free space") errHomeDiskNoSpace = errors.New("home disk has insufficient free space")
@ -658,23 +659,19 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
tempIndexFolders := make([]string, 0, len(cm.Folders)) tempIndexFolders := make([]string, 0, len(cm.Folders))
m.fmut.Lock()
nextFolder:
for _, folder := range cm.Folders { for _, folder := range cm.Folders {
cfg := m.folderCfgs[folder.ID]
if folder.Flags&^protocol.FlagFolderAll != 0 { if folder.Flags&^protocol.FlagFolderAll != 0 {
// There are flags set that we don't know what they mean. Scary! // There are flags set that we don't know what they mean. Fatal!
l.Warnf("Device %v: unknown flags for folder %s", deviceID, folder.ID) l.Warnf("Device %v: unknown flags for folder %s", deviceID, folder.ID)
cfg.Invalid = fmt.Sprintf("Unknown flags from device %v", deviceID) m.fmut.Unlock()
m.cfg.SetFolder(cfg) m.Close(deviceID, fmt.Errorf("Unknown folder flags from device %v", deviceID))
if srv := m.folderRunners[folder.ID]; srv != nil { return
srv.setError(fmt.Errorf(cfg.Invalid))
}
continue nextFolder
} }
if !m.folderSharedWithUnlocked(folder.ID, deviceID) { m.fmut.Lock()
shared := m.folderSharedWithUnlocked(folder.ID, deviceID)
m.fmut.Unlock()
if !shared {
events.Default.Log(events.FolderRejected, map[string]string{ events.Default.Log(events.FolderRejected, map[string]string{
"folder": folder.ID, "folder": folder.ID,
"folderLabel": folder.Label, "folderLabel": folder.Label,
@ -687,7 +684,6 @@ nextFolder:
tempIndexFolders = append(tempIndexFolders, folder.ID) tempIndexFolders = append(tempIndexFolders, folder.ID)
} }
} }
m.fmut.Unlock()
// This breaks if we send multiple CM messages during the same connection. // This breaks if we send multiple CM messages during the same connection.
if len(tempIndexFolders) > 0 { if len(tempIndexFolders) > 0 {
@ -1953,6 +1949,10 @@ func (m *Model) CheckFolderHealth(id string) error {
// checkFolderPath returns nil if the folder path exists and has the marker file. // checkFolderPath returns nil if the folder path exists and has the marker file.
func (m *Model) checkFolderPath(folder config.FolderConfiguration) error { func (m *Model) checkFolderPath(folder config.FolderConfiguration) error {
if folder.Path() == "" {
return errFolderPathEmpty
}
if fi, err := os.Stat(folder.Path()); err != nil || !fi.IsDir() { if fi, err := os.Stat(folder.Path()); err != nil || !fi.IsDir() {
return errFolderPathMissing return errFolderPathMissing
} }

View File

@ -642,9 +642,6 @@ func TestROScanRecovery(t *testing.T) {
waitFor := func(status string) error { waitFor := func(status string) error {
timeout := time.Now().Add(2 * time.Second) timeout := time.Now().Add(2 * time.Second)
for { for {
if time.Now().After(timeout) {
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
}
_, _, err := m.State("default") _, _, err := m.State("default")
if err == nil && status == "" { if err == nil && status == "" {
return nil return nil
@ -652,6 +649,10 @@ func TestROScanRecovery(t *testing.T) {
if err != nil && err.Error() == status { if err != nil && err.Error() == status {
return nil return nil
} }
if time.Now().After(timeout) {
return fmt.Errorf("Timed out waiting for status: %s, current status: %v", status, err)
}
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
} }
@ -727,9 +728,6 @@ func TestRWScanRecovery(t *testing.T) {
waitFor := func(status string) error { waitFor := func(status string) error {
timeout := time.Now().Add(2 * time.Second) timeout := time.Now().Add(2 * time.Second)
for { for {
if time.Now().After(timeout) {
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
}
_, _, err := m.State("default") _, _, err := m.State("default")
if err == nil && status == "" { if err == nil && status == "" {
return nil return nil
@ -737,6 +735,10 @@ func TestRWScanRecovery(t *testing.T) {
if err != nil && err.Error() == status { if err != nil && err.Error() == status {
return nil return nil
} }
if time.Now().After(timeout) {
return fmt.Errorf("Timed out waiting for status: %s, current status: %v", status, err)
}
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
} }