lib/model: Fix child-check when deleting dirs in pull (#7236)

This commit is contained in:
Simon Frei 2021-01-02 21:40:37 +01:00 committed by GitHub
parent 0f8290485e
commit c48eb4241a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 122 additions and 70 deletions

View File

@ -1878,76 +1878,8 @@ func (f *sendReceiveFolder) deleteDirOnDisk(dir string, snap *db.Snapshot, scanC
return err
}
files, _ := f.mtimefs.DirNames(dir)
toBeDeleted := make([]string, 0, len(files))
var hasIgnored, hasKnown, hasToBeScanned, hasReceiveOnlyChanged bool
for _, dirFile := range files {
fullDirFile := filepath.Join(dir, dirFile)
switch {
case fs.IsTemporary(dirFile) || f.ignores.Match(fullDirFile).IsDeletable():
toBeDeleted = append(toBeDeleted, fullDirFile)
continue
case f.ignores != nil && f.ignores.Match(fullDirFile).IsIgnored():
hasIgnored = true
continue
}
cf, ok := snap.Get(protocol.LocalDeviceID, fullDirFile)
switch {
case !ok || cf.IsDeleted():
// Something appeared in the dir that we either are not
// aware of at all or that we think should be deleted
// -> schedule scan.
scanChan <- fullDirFile
hasToBeScanned = true
continue
case ok && f.Type == config.FolderTypeReceiveOnly && cf.IsReceiveOnlyChanged():
hasReceiveOnlyChanged = true
continue
}
info, err := f.mtimefs.Lstat(fullDirFile)
var diskFile protocol.FileInfo
if err == nil {
diskFile, err = scanner.CreateFileInfo(info, fullDirFile, f.mtimefs)
}
if err != nil {
// Lets just assume the file has changed.
scanChan <- fullDirFile
hasToBeScanned = true
continue
}
if !cf.IsEquivalentOptional(diskFile, f.modTimeWindow, f.IgnorePerms, true, protocol.LocalAllFlags) {
// File on disk changed compared to what we have in db
// -> schedule scan.
scanChan <- fullDirFile
hasToBeScanned = true
continue
}
// Dir contains file that is valid according to db and
// not ignored -> something weird is going on
hasKnown = true
}
if hasToBeScanned {
return errDirHasToBeScanned
}
if hasIgnored {
return errDirHasIgnored
}
if hasReceiveOnlyChanged {
// Pretend we deleted the directory. It will be resurrected as a
// receive-only changed item on scan.
scanChan <- dir
return nil
}
if hasKnown {
return errDirNotEmpty
}
for _, del := range toBeDeleted {
f.mtimefs.RemoveAll(del)
if err := f.deleteDirOnDiskHandleChildren(dir, snap, scanChan); err != nil {
return err
}
err := f.inWritableDir(f.mtimefs.Remove, dir)
@ -1966,6 +1898,99 @@ func (f *sendReceiveFolder) deleteDirOnDisk(dir string, snap *db.Snapshot, scanC
return err
}
func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, snap *db.Snapshot, scanChan chan<- string) error {
var dirsToDelete []string
var hasIgnored, hasKnown, hasToBeScanned, hasReceiveOnlyChanged bool
var delErr error
err := f.mtimefs.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if path == dir {
return nil
}
if err != nil {
return err
}
switch match := f.ignores.Match(path); {
case match.IsDeletable():
if info.IsDir() {
dirsToDelete = append(dirsToDelete, path)
return nil
}
fallthrough
case fs.IsTemporary(path):
if err := f.mtimefs.Remove(path); err != nil && delErr == nil {
delErr = err
}
return nil
case match.IsIgnored():
hasIgnored = true
return nil
}
cf, ok := snap.Get(protocol.LocalDeviceID, path)
switch {
case !ok || cf.IsDeleted():
// Something appeared in the dir that we either are not
// aware of at all or that we think should be deleted
// -> schedule scan.
scanChan <- path
hasToBeScanned = true
return nil
case ok && f.Type == config.FolderTypeReceiveOnly && cf.IsReceiveOnlyChanged():
hasReceiveOnlyChanged = true
return nil
}
diskFile, err := scanner.CreateFileInfo(info, path, f.mtimefs)
if err != nil {
// Lets just assume the file has changed.
scanChan <- path
hasToBeScanned = true
return nil
}
if !cf.IsEquivalentOptional(diskFile, f.modTimeWindow, f.IgnorePerms, true, protocol.LocalAllFlags) {
// File on disk changed compared to what we have in db
// -> schedule scan.
scanChan <- path
hasToBeScanned = true
return nil
}
// Dir contains file that is valid according to db and
// not ignored -> something weird is going on
hasKnown = true
return nil
})
if err != nil {
return err
}
for i := range dirsToDelete {
if err := f.mtimefs.Remove(dirsToDelete[len(dirsToDelete)-1-i]); err != nil && delErr == nil {
delErr = err
}
}
// "Error precedence":
// Something changed on disk, check that and maybe all else gets resolved
if hasToBeScanned {
return errDirHasToBeScanned
}
// Ignored files will never be touched, i.e. this will keep failing until
// user acts.
if hasIgnored {
return errDirHasIgnored
}
if hasReceiveOnlyChanged {
// Pretend we deleted the directory. It will be resurrected as a
// receive-only changed item on scan.
scanChan <- dir
return nil
}
if hasKnown {
return errDirNotEmpty
}
// All good, except maybe failing to remove a (?d) ignored item
return delErr
}
// scanIfItemChanged schedules the given file for scanning and returns errModified
// if it differs from the information in the database. Returns nil if the file has
// not changed.

View File

@ -11,6 +11,7 @@ import (
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
@ -1350,6 +1351,32 @@ func TestPullDeleteCaseConflict(t *testing.T) {
}
}
func TestPullDeleteIgnoreChildDir(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
parent := "parent"
del := "ignored"
child := "keep"
matcher := ignore.New(f.mtimefs)
must(t, matcher.Parse(bytes.NewBufferString(fmt.Sprintf(`
!%v
(?d)%v
`, child, del)), ""))
f.ignores = matcher
must(t, f.mtimefs.Mkdir(parent, 0777))
must(t, f.mtimefs.Mkdir(filepath.Join(parent, del), 0777))
must(t, f.mtimefs.Mkdir(filepath.Join(parent, del, child), 0777))
scanChan := make(chan string, 2)
err := f.deleteDirOnDisk(parent, f.fset.Snapshot(), scanChan)
if err == nil {
t.Error("no error")
}
}
func cleanupSharedPullerState(s *sharedPullerState) {
s.mut.Lock()
defer s.mut.Unlock()