diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 2e8b3483e..587b8c0e5 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -575,17 +575,11 @@ func setupGUI(cfg *config.ConfigWrapper, m *model.Model) { } func sanityCheckFolders(cfg *config.ConfigWrapper, m *model.Model) { - var err error - nextFolder: for id, folder := range cfg.Folders() { if folder.Invalid != "" { continue } - folder.Path, err = osutil.ExpandTilde(folder.Path) - if err != nil { - l.Fatalln("home:", err) - } m.AddFolder(folder) fi, err := os.Stat(folder.Path) @@ -598,11 +592,25 @@ nextFolder: l.Warnf("Stopping folder %q - path does not exist, but has files in index", folder.ID) cfg.InvalidateFolder(id, "folder path missing") continue nextFolder + } else if !folder.HasMarker() { + l.Warnf("Stopping folder %q - path exists, but folder marker missing, check for mount issues", folder.ID) + cfg.InvalidateFolder(id, "folder marker missing") + continue nextFolder } } else if os.IsNotExist(err) { // If we don't have any files in the index, and the directory // doesn't exist, try creating it. err = os.MkdirAll(folder.Path, 0700) + if err != nil { + l.Warnf("Stopping folder %q - %v", err) + cfg.InvalidateFolder(id, err.Error()) + continue nextFolder + } + err = folder.CreateMarker() + } else if !folder.HasMarker() { + // If we don't have any files in the index, and the path does exist + // but the marker is not there, create it. + err = folder.CreateMarker() } if err != nil { diff --git a/cmd/syncthing/main_test.go b/cmd/syncthing/main_test.go index a36f8f723..b3df6da03 100644 --- a/cmd/syncthing/main_test.go +++ b/cmd/syncthing/main_test.go @@ -13,6 +13,107 @@ // You should have received a copy of the GNU General Public License along // with this program. If not, see . -package main_test +package main -// Empty test file to generate 0% coverage rather than no coverage +import ( + "os" + "testing" + + "github.com/syncthing/syncthing/internal/config" + "github.com/syncthing/syncthing/internal/files" + "github.com/syncthing/syncthing/internal/model" + "github.com/syncthing/syncthing/internal/protocol" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/storage" +) + +func TestSanityCheck(t *testing.T) { + fcfg := config.FolderConfiguration{ + ID: "folder", + Path: "testdata/testfolder", + } + cfg := config.Wrap("/tmp/test", config.Configuration{ + Folders: []config.FolderConfiguration{fcfg}, + }) + + for _, file := range []string{".stfolder", "testfolder", "testfolder/.stfolder"} { + _, err := os.Stat("testdata/" + file) + if err == nil { + t.Error("Found unexpected file") + } + } + + db, _ := leveldb.Open(storage.NewMemStorage(), nil) + + // Case 1 - new folder, directory and marker created + + m := model.NewModel(cfg, "device", "syncthing", "dev", db) + sanityCheckFolders(cfg, m) + + if cfg.Folders()["folder"].Invalid != "" { + t.Error("Unexpected error", cfg.Folders()["folder"].Invalid) + } + + s, err := os.Stat("testdata/testfolder") + if err != nil || !s.IsDir() { + t.Error(err) + } + + _, err = os.Stat("testdata/testfolder/.stfolder") + if err != nil { + t.Error(err) + } + + os.Remove("testdata/testfolder/.stfolder") + os.Remove("testdata/testfolder/") + + // Case 2 - new folder, marker created + + fcfg.Path = "testdata/" + cfg = config.Wrap("/tmp/test", config.Configuration{ + Folders: []config.FolderConfiguration{fcfg}, + }) + + m = model.NewModel(cfg, "device", "syncthing", "dev", db) + sanityCheckFolders(cfg, m) + + if cfg.Folders()["folder"].Invalid != "" { + t.Error("Unexpected error", cfg.Folders()["folder"].Invalid) + } + + _, err = os.Stat("testdata/.stfolder") + if err != nil { + t.Error(err) + } + + os.Remove("testdata/.stfolder") + + // Case 3 - marker missing + + set := files.NewSet("folder", db) + set.Update(protocol.LocalDeviceID, []protocol.FileInfo{ + {Name: "dummyfile"}, + }) + + m = model.NewModel(cfg, "device", "syncthing", "dev", db) + sanityCheckFolders(cfg, m) + + if cfg.Folders()["folder"].Invalid != "folder marker missing" { + t.Error("Incorrect error") + } + + // Case 4 - path missing + + fcfg.Path = "testdata/testfolder" + cfg = config.Wrap("/tmp/test", config.Configuration{ + Folders: []config.FolderConfiguration{fcfg}, + }) + + m = model.NewModel(cfg, "device", "syncthing", "dev", db) + sanityCheckFolders(cfg, m) + + if cfg.Folders()["folder"].Invalid != "folder path missing" { + t.Error("Incorrect error") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 58ccc9f3c..8d90d9200 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,18 +21,20 @@ import ( "fmt" "io" "os" + "path/filepath" "reflect" "sort" "strconv" "code.google.com/p/go.crypto/bcrypt" "github.com/syncthing/syncthing/internal/logger" + "github.com/syncthing/syncthing/internal/osutil" "github.com/syncthing/syncthing/internal/protocol" ) var l = logger.DefaultLogger -const CurrentVersion = 5 +const CurrentVersion = 6 type Configuration struct { Version int `xml:"version,attr"` @@ -64,6 +66,28 @@ type FolderConfiguration struct { Deprecated_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"` } +func (f *FolderConfiguration) CreateMarker() error { + if !f.HasMarker() { + marker := filepath.Join(f.Path, ".stfolder") + fd, err := os.Create(marker) + if err != nil { + return err + } + fd.Close() + osutil.HideFile(marker) + } + + return nil +} + +func (f *FolderConfiguration) HasMarker() bool { + _, err := os.Stat(filepath.Join(f.Path, ".stfolder")) + if err != nil { + return false + } + return true +} + func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID { if r.deviceIDs == nil { for _, n := range r.Devices { @@ -272,6 +296,11 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) { convertV4V5(cfg) } + // Upgrade to v6 configuration if appropriate + if cfg.Version == 5 { + convertV5V6(cfg) + } + // Hash old cleartext passwords if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' { hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0) @@ -344,6 +373,20 @@ func ChangeRequiresRestart(from, to Configuration) bool { return false } +func convertV5V6(cfg *Configuration) { + // Added ".stfolder" file at folder roots to identify mount issues + // Doesn't affect the config itself, but uses config migrations to identify + // the migration point. + for _, folder := range Wrap("", *cfg).Folders() { + err := folder.CreateMarker() + if err != nil { + panic(err) + } + } + + cfg.Version = 6 +} + func convertV4V5(cfg *Configuration) { // Renamed a bunch of fields in the structs. if cfg.Deprecated_Nodes == nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9c4102539..216b05f17 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -16,6 +16,7 @@ package config import ( + "fmt" "os" "reflect" "testing" @@ -60,17 +61,26 @@ func TestDefaultValues(t *testing.T) { } func TestDeviceConfig(t *testing.T) { - for i, ver := range []string{"v1", "v2", "v3", "v4", "v5"} { - wr, err := Load("testdata/"+ver+".xml", device1) + for i := 1; i <= CurrentVersion; i++ { + os.Remove("testdata/.stfolder") + wr, err := Load(fmt.Sprintf("testdata/v%d.xml", i), device1) if err != nil { t.Fatal(err) } + + _, err = os.Stat("testdata/.stfolder") + if i < 6 && err != nil { + t.Fatal(err) + } else if i >= 6 && err == nil { + t.Fatal("Unexpected file") + } + cfg := wr.cfg expectedFolders := []FolderConfiguration{ { ID: "test", - Path: "~/Sync", + Path: "testdata/", Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}}, ReadOnly: true, RescanIntervalS: 600, @@ -92,8 +102,8 @@ func TestDeviceConfig(t *testing.T) { } expectedDeviceIDs := []protocol.DeviceID{device1, device4} - if cfg.Version != 5 { - t.Errorf("%d: Incorrect version %d != 5", i, cfg.Version) + if cfg.Version != CurrentVersion { + t.Errorf("%d: Incorrect version %d != %d", i, cfg.Version, CurrentVersion) } if !reflect.DeepEqual(cfg.Folders, expectedFolders) { t.Errorf("%d: Incorrect Folders\n A: %#v\n E: %#v", i, cfg.Folders, expectedFolders) @@ -296,7 +306,7 @@ func TestPrepare(t *testing.T) { } func TestRequiresRestart(t *testing.T) { - wr, err := Load("testdata/v5.xml", device1) + wr, err := Load("testdata/v6.xml", device1) if err != nil { t.Fatal(err) } diff --git a/internal/config/testdata/.stfolder b/internal/config/testdata/.stfolder new file mode 100644 index 000000000..e69de29bb diff --git a/internal/config/testdata/v1.xml b/internal/config/testdata/v1.xml index 2365effbb..a75154530 100755 --- a/internal/config/testdata/v1.xml +++ b/internal/config/testdata/v1.xml @@ -1,5 +1,5 @@ - +
a
diff --git a/internal/config/testdata/v2.xml b/internal/config/testdata/v2.xml index bfddd25ea..ef0fd7cfb 100755 --- a/internal/config/testdata/v2.xml +++ b/internal/config/testdata/v2.xml @@ -1,5 +1,5 @@ - + diff --git a/internal/config/testdata/v3.xml b/internal/config/testdata/v3.xml index 0f527fae6..5bd748500 100755 --- a/internal/config/testdata/v3.xml +++ b/internal/config/testdata/v3.xml @@ -1,5 +1,5 @@ - + diff --git a/internal/config/testdata/v4.xml b/internal/config/testdata/v4.xml index 73d6649d8..c0a8e0010 100755 --- a/internal/config/testdata/v4.xml +++ b/internal/config/testdata/v4.xml @@ -1,5 +1,5 @@ - + diff --git a/internal/config/testdata/v5.xml b/internal/config/testdata/v5.xml index c09723aad..7daa88e14 100755 --- a/internal/config/testdata/v5.xml +++ b/internal/config/testdata/v5.xml @@ -1,5 +1,5 @@ - + diff --git a/internal/config/testdata/v6.xml b/internal/config/testdata/v6.xml new file mode 100644 index 000000000..ae73c0bc5 --- /dev/null +++ b/internal/config/testdata/v6.xml @@ -0,0 +1,12 @@ + + + + + + +
a
+
+ +
b
+
+
diff --git a/internal/config/testdata/versioningconfig.xml b/internal/config/testdata/versioningconfig.xml index c98be30b2..137b84be6 100755 --- a/internal/config/testdata/versioningconfig.xml +++ b/internal/config/testdata/versioningconfig.xml @@ -1,5 +1,5 @@ - + diff --git a/internal/config/wrapper.go b/internal/config/wrapper.go index e83aac4e3..b5070789b 100644 --- a/internal/config/wrapper.go +++ b/internal/config/wrapper.go @@ -167,6 +167,12 @@ func (w *ConfigWrapper) Folders() map[string]FolderConfiguration { if w.folderMap == nil { w.folderMap = make(map[string]FolderConfiguration, len(w.cfg.Folders)) for _, fld := range w.cfg.Folders { + path, err := osutil.ExpandTilde(fld.Path) + if err != nil { + l.Warnln("home:", err) + continue + } + fld.Path = path w.folderMap[fld.ID] = fld } } diff --git a/internal/scanner/walk.go b/internal/scanner/walk.go index 5208a13bc..cd7657aff 100644 --- a/internal/scanner/walk.go +++ b/internal/scanner/walk.go @@ -113,7 +113,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun return nil } - if sn := filepath.Base(rn); sn == ".stignore" || sn == ".stversions" || w.Ignores.Match(rn) { + if sn := filepath.Base(rn); sn == ".stignore" || sn == ".stversions" || sn == ".stfolder" || w.Ignores.Match(rn) { // An ignored file if debug { l.Debugln("ignored:", rn)