diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go index a2da02c4..d32a389b 100644 --- a/lib/model/folder_sendrecv.go +++ b/lib/model/folder_sendrecv.go @@ -847,10 +847,17 @@ func (f *sendReceiveFolder) deleteFileWithCurrent(file, cur protocol.FileInfo, h if !hasCur { // We should never try to pull a deletion for a file we don't have in the DB. - l.Debugln(f, "not deleting file we don't have", file.Name) + l.Debugln(f, "not deleting file we don't have, but update db", file.Name) dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteFile} return } + + if err = osutil.TraversesSymlink(f.fs, filepath.Dir(file.Name)); err != nil { + l.Debugln(f, "not deleting file behind symlink on disk, but update db", file.Name) + dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteFile} + return + } + if err = f.checkToBeDeleted(cur, scanChan); err != nil { return } @@ -1839,6 +1846,10 @@ func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, scanChan ch // deleteDirOnDisk attempts to delete a directory. It checks for files/dirs inside // the directory and removes them if possible or returns an error if it fails func (f *sendReceiveFolder) deleteDirOnDisk(dir string, scanChan chan<- string) error { + if err := osutil.TraversesSymlink(f.fs, filepath.Dir(dir)); err != nil { + return err + } + files, _ := f.fs.DirNames(dir) toBeDeleted := make([]string, 0, len(files)) diff --git a/lib/model/folder_sendrecv_test.go b/lib/model/folder_sendrecv_test.go index 6deb8712..fd0a8010 100644 --- a/lib/model/folder_sendrecv_test.go +++ b/lib/model/folder_sendrecv_test.go @@ -23,6 +23,7 @@ import ( "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/ignore" + "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/scanner" "github.com/syncthing/syncthing/lib/sync" @@ -124,7 +125,7 @@ func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFol func cleanupSRFolder(f *sendReceiveFolder, m *model) { m.evLogger.Stop() os.Remove(m.cfg.ConfigPath()) - os.Remove(f.Filesystem().URI()) + os.RemoveAll(f.Filesystem().URI()) } // Layout of the files: (indexes from the above array) @@ -907,6 +908,58 @@ func TestSRConflictReplaceFileByLink(t *testing.T) { } } +// TestDeleteBehindSymlink checks that we don't delete or schedule a scan +// when trying to delete a file behind a symlink. +func TestDeleteBehindSymlink(t *testing.T) { + m, f := setupSendReceiveFolder() + defer cleanupSRFolder(f, m) + ffs := f.Filesystem() + + destDir := createTmpDir() + defer os.RemoveAll(destDir) + destFs := fs.NewFilesystem(fs.FilesystemTypeBasic, destDir) + + link := "link" + file := filepath.Join(link, "file") + + must(t, ffs.MkdirAll(link, 0755)) + fi := createFile(t, file, ffs) + f.updateLocalsFromScanning([]protocol.FileInfo{fi}) + must(t, osutil.RenameOrCopy(ffs, destFs, file, "file")) + must(t, ffs.RemoveAll(link)) + + if err := osutil.DebugSymlinkForTestsOnly(destFs.URI(), filepath.Join(ffs.URI(), link)); err != nil { + if runtime.GOOS == "windows" { + // Probably we require permissions we don't have. + t.Skip("Need admin permissions or developer mode to run symlink test on Windows: " + err.Error()) + } else { + t.Fatal(err) + } + } + + fi.Deleted = true + fi.Version = fi.Version.Update(device1.Short()) + scanChan := make(chan string, 1) + dbUpdateChan := make(chan dbUpdateJob, 1) + f.deleteFile(fi, dbUpdateChan, scanChan) + select { + case f := <-scanChan: + t.Fatalf("Received %v on scanChan", f) + case u := <-dbUpdateChan: + if u.jobType != dbUpdateDeleteFile { + t.Errorf("Expected jobType %v, got %v", dbUpdateDeleteFile, u.jobType) + } + if u.file.Name != fi.Name { + t.Errorf("Expected update for %v, got %v", fi.Name, u.file.Name) + } + default: + t.Fatalf("No db update received") + } + if _, err := destFs.Stat("file"); err != nil { + t.Errorf("Expected no error when stating file behind symlink, got %v", err) + } +} + func cleanupSharedPullerState(s *sharedPullerState) { s.mut.Lock() defer s.mut.Unlock()