When scanner.Walk detects a change, it now returns the new file info as well as the old file info. It also finds deleted and ignored files while scanning. Also directory deletions are now always committed to db after their children to prevent temporary failure on remote due to non-empty directory.
This commit is contained in:
@@ -1395,14 +1395,21 @@ func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
|
||||
return fs.GetGlobal(file)
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
m *Model
|
||||
r string
|
||||
type haveWalker struct {
|
||||
fset *db.FileSet
|
||||
}
|
||||
|
||||
// Implements scanner.CurrentFiler
|
||||
func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
||||
return cf.m.CurrentFolderFile(cf.r, file)
|
||||
func (h haveWalker) Walk(prefix string, ctx context.Context, out chan<- protocol.FileInfo) {
|
||||
ctxChan := ctx.Done()
|
||||
h.fset.WithPrefixedHave(protocol.LocalDeviceID, prefix, func(fi db.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
select {
|
||||
case out <- f:
|
||||
case <-ctxChan:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Connection returns the current connection for device, and a boolean wether a connection was found.
|
||||
@@ -1955,13 +1962,14 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
|
||||
fchan := scanner.Walk(ctx, scanner.Config{
|
||||
haveWalker := haveWalker{fset}
|
||||
rchan := scanner.Walk(ctx, scanner.Config{
|
||||
Folder: folderCfg.ID,
|
||||
Subs: subDirs,
|
||||
Matcher: ignores,
|
||||
BlockSize: protocol.BlockSize,
|
||||
TempLifetime: time.Duration(m.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{m, folder},
|
||||
Have: haveWalker,
|
||||
Filesystem: mtimefs,
|
||||
IgnorePerms: folderCfg.IgnorePerms,
|
||||
AutoNormalize: folderCfg.AutoNormalize,
|
||||
@@ -1978,6 +1986,17 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
|
||||
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
|
||||
batchSizeBytes := 0
|
||||
changes := 0
|
||||
checkBatch := func() error {
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Schedule a pull after scanning, but only if we actually detected any
|
||||
// changes.
|
||||
@@ -1987,98 +2006,49 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
|
||||
}
|
||||
}()
|
||||
|
||||
for f := range fchan {
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
var delDirStack []protocol.FileInfo
|
||||
for r := range rchan {
|
||||
if err := checkBatch(); err != nil {
|
||||
l.Debugln("Stopping scan of folder %s due to: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Append deleted dirs from stack if the current file isn't a child,
|
||||
// which means all children were already processed.
|
||||
for len(delDirStack) != 0 && !strings.HasPrefix(r.New.Name, delDirStack[len(delDirStack)-1].Name+string(fs.PathSeparator)) {
|
||||
lastDelDir := delDirStack[len(delDirStack)-1]
|
||||
batch = append(batch, lastDelDir)
|
||||
batchSizeBytes += lastDelDir.ProtoSize()
|
||||
changes++
|
||||
if err := checkBatch(); err != nil {
|
||||
l.Debugln("Stopping scan of folder %s due to: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
delDirStack = delDirStack[:len(delDirStack)-1]
|
||||
}
|
||||
|
||||
batch = append(batch, f)
|
||||
batchSizeBytes += f.ProtoSize()
|
||||
// Delay appending deleted dirs until all its children are processed
|
||||
if r.Old.IsDirectory() && (r.New.Deleted || !r.New.IsDirectory()) {
|
||||
delDirStack = append(delDirStack, r.New)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugln("Appending", r)
|
||||
batch = append(batch, r.New)
|
||||
batchSizeBytes += r.New.ProtoSize()
|
||||
changes++
|
||||
}
|
||||
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
l.Debugln("Stopping scan of folder %s due to: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
} else if len(batch) > 0 {
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
}
|
||||
|
||||
if len(subDirs) == 0 {
|
||||
// If we have no specific subdirectories to traverse, set it to one
|
||||
// empty prefix so we traverse the entire folder contents once.
|
||||
subDirs = []string{""}
|
||||
}
|
||||
|
||||
// Do a scan of the database for each prefix, to check for deleted and
|
||||
// ignored files.
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
for _, sub := range subDirs {
|
||||
var iterError error
|
||||
|
||||
fset.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
f := fi.(db.FileInfoTruncated)
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
|
||||
switch {
|
||||
case !f.IsInvalid() && ignores.Match(f.Name).IsIgnored():
|
||||
// File was valid at last pass but has been ignored. Set invalid bit.
|
||||
l.Debugln("setting invalid bit on ignored", f)
|
||||
nf := f.ConvertToInvalidFileInfo(m.id.Short())
|
||||
batch = append(batch, nf)
|
||||
batchSizeBytes += nf.ProtoSize()
|
||||
changes++
|
||||
|
||||
case !f.IsInvalid() && !f.IsDeleted():
|
||||
// The file is valid and not deleted. Lets check if it's
|
||||
// still here.
|
||||
|
||||
if _, err := mtimefs.Lstat(f.Name); err != nil {
|
||||
// We don't specifically verify that the error is
|
||||
// fs.IsNotExist because there is a corner case when a
|
||||
// directory is suddenly transformed into a file. When that
|
||||
// happens, files that were in the directory (that is now a
|
||||
// file) are deleted but will return a confusing error ("not a
|
||||
// directory") when we try to Lstat() them.
|
||||
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Size: 0,
|
||||
ModifiedS: f.ModifiedS,
|
||||
ModifiedNs: f.ModifiedNs,
|
||||
ModifiedBy: m.id.Short(),
|
||||
Deleted: true,
|
||||
Version: f.Version.Update(m.shortID),
|
||||
}
|
||||
|
||||
batch = append(batch, nf)
|
||||
batchSizeBytes += nf.ProtoSize()
|
||||
changes++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if iterError != nil {
|
||||
l.Debugln("Stopping scan of folder %s due to: %s", folderCfg.Description(), iterError)
|
||||
return iterError
|
||||
// Append remaining deleted dirs.
|
||||
for i := len(delDirStack) - 1; i >= 0; i-- {
|
||||
if err := checkBatch(); err != nil {
|
||||
l.Debugln("Stopping scan of folder %s due to: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
batch = append(batch, delDirStack[i])
|
||||
batchSizeBytes += delDirStack[i].ProtoSize()
|
||||
changes++
|
||||
}
|
||||
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
|
||||
@@ -2808,6 +2808,185 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssue2571 tests replacing a directory with content with a symlink
|
||||
func TestIssue2571(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Symlinks aren't supported by fs and scanner on windows")
|
||||
}
|
||||
err := defaultFs.MkdirAll("replaceDir", 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
defaultFs.RemoveAll("replaceDir")
|
||||
}()
|
||||
|
||||
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(defaultFs.URI(), "replaceDir"))
|
||||
|
||||
for _, dir := range []string{"toLink", "linkTarget"} {
|
||||
err := testFs.MkdirAll(dir, 0775)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd, err := testFs.Create(filepath.Join(dir, "a"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
dbi := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
if err = testFs.RemoveAll("toLink"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := osutil.DebugSymlinkForTestsOnly(filepath.Join(testFs.URI(), "linkTarget"), filepath.Join(testFs.URI(), "toLink")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m.ScanFolder("default")
|
||||
|
||||
if dir, ok := m.CurrentFolderFile("default", filepath.Join("replaceDir", "toLink")); !ok {
|
||||
t.Fatalf("Dir missing in db")
|
||||
} else if !dir.IsSymlink() {
|
||||
t.Errorf("Dir wasn't changed to symlink")
|
||||
}
|
||||
if file, ok := m.CurrentFolderFile("default", filepath.Join("replaceDir", "toLink", "a")); !ok {
|
||||
t.Fatalf("File missing in db")
|
||||
} else if !file.Deleted {
|
||||
t.Errorf("File below symlink has not been marked as deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssue4573 tests that contents of an unavailable dir aren't marked deleted
|
||||
func TestIssue4573(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Can't make the dir inaccessible on windows")
|
||||
}
|
||||
|
||||
err := defaultFs.MkdirAll("inaccessible", 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
defaultFs.Chmod("inaccessible", 0777)
|
||||
defaultFs.RemoveAll("inaccessible")
|
||||
}()
|
||||
|
||||
file := filepath.Join("inaccessible", "a")
|
||||
fd, err := defaultFs.Create(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
dbi := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
err = defaultFs.Chmod("inaccessible", 0000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m.ScanFolder("default")
|
||||
|
||||
if file, ok := m.CurrentFolderFile("default", file); !ok {
|
||||
t.Fatalf("File missing in db")
|
||||
} else if file.Deleted {
|
||||
t.Errorf("Inaccessible file has been marked as deleted.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInternalScan checks whether various fs operations are correctly represented
|
||||
// in the db after scanning.
|
||||
func TestInternalScan(t *testing.T) {
|
||||
err := defaultFs.MkdirAll("internalScan", 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
defaultFs.RemoveAll("internalScan")
|
||||
}()
|
||||
|
||||
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(defaultFs.URI(), "internalScan"))
|
||||
|
||||
testCases := map[string]func(protocol.FileInfo) bool{
|
||||
"removeDir": func(f protocol.FileInfo) bool {
|
||||
return !f.Deleted
|
||||
},
|
||||
"dirToFile": func(f protocol.FileInfo) bool {
|
||||
return f.Deleted || f.IsDirectory()
|
||||
},
|
||||
}
|
||||
|
||||
baseDirs := []string{"dirToFile", "removeDir"}
|
||||
for _, dir := range baseDirs {
|
||||
sub := filepath.Join(dir, "subDir")
|
||||
for _, dir := range []string{dir, sub} {
|
||||
err := testFs.MkdirAll(dir, 0775)
|
||||
if err != nil {
|
||||
t.Fatalf("%v: %v", dir, err)
|
||||
}
|
||||
}
|
||||
testCases[sub] = func(f protocol.FileInfo) bool {
|
||||
return !f.Deleted
|
||||
}
|
||||
for _, dir := range []string{dir, sub} {
|
||||
file := filepath.Join(dir, "a")
|
||||
fd, err := testFs.Create(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Close()
|
||||
testCases[file] = func(f protocol.FileInfo) bool {
|
||||
return !f.Deleted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbi := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
for _, dir := range baseDirs {
|
||||
if err = testFs.RemoveAll(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := testFs.Create("dirToFile")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
m.ScanFolder("default")
|
||||
|
||||
for path, cond := range testCases {
|
||||
if f, ok := m.CurrentFolderFile("default", filepath.Join("internalScan", path)); !ok {
|
||||
t.Fatalf("%v missing in db", path)
|
||||
} else if cond(f) {
|
||||
t.Errorf("Incorrect db entry for %v", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomMarkerName(t *testing.T) {
|
||||
ldb := db.OpenMemory()
|
||||
set := db.NewFileSet("default", defaultFs, ldb)
|
||||
|
||||
Reference in New Issue
Block a user