mirror of
https://github.com/syncthing/syncthing.git
synced 2024-11-16 10:28:49 -07:00
Merge pull request #2337 from calmh/caseins
Case insensitive renames, part 1
This commit is contained in:
commit
460cb19839
@ -1370,6 +1370,7 @@ nextSub:
|
||||
// TODO: We should limit the Have scanning to start at sub
|
||||
seenPrefix := false
|
||||
var iterError error
|
||||
css := osutil.NewCachedCaseSensitiveStat(folderCfg.Path())
|
||||
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
f := fi.(db.FileInfoTruncated)
|
||||
hasPrefix := len(subs) == 0
|
||||
@ -1413,7 +1414,7 @@ nextSub:
|
||||
Version: f.Version, // The file is still the same, so don't bump version
|
||||
}
|
||||
batch = append(batch, nf)
|
||||
} else if _, err := osutil.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
} else if _, err := css.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
// File has been deleted.
|
||||
|
||||
// We don't specifically verify that the error is
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -241,3 +242,90 @@ func SetTCPOptions(conn *net.TCPConn) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The CachedCaseSensitiveStat provides an Lstat() method similar to
|
||||
// os.Lstat(), but that is always case sensitive regardless of underlying file
|
||||
// system semantics. The "Cached" part refers to the fact that it lists the
|
||||
// contents of a directory the first time it's needed and then retains this
|
||||
// information for the duration. It's expected that instances of this type are
|
||||
// fairly short lived.
|
||||
//
|
||||
// There's some song and dance to check directories that are parents to the
|
||||
// checked path as well, that is we want to catch the situation that someone
|
||||
// calls Lstat("foo/BAR/baz") when the actual path is "foo/bar/baz" and return
|
||||
// NotExist appropriately. But we don't want to do this check too high up, as
|
||||
// the user may have told us the folder path is ~/Sync while it is actually
|
||||
// ~/sync and this *should* work properly... Sigh. Hence the "base" parameter.
|
||||
type CachedCaseSensitiveStat struct {
|
||||
base string // base directory, we should not check stuff above this
|
||||
results map[string][]os.FileInfo // directory path => list of children
|
||||
}
|
||||
|
||||
func NewCachedCaseSensitiveStat(base string) *CachedCaseSensitiveStat {
|
||||
return &CachedCaseSensitiveStat{
|
||||
base: strings.ToLower(base),
|
||||
results: make(map[string][]os.FileInfo),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedCaseSensitiveStat) Lstat(name string) (os.FileInfo, error) {
|
||||
dir := filepath.Dir(name)
|
||||
base := filepath.Base(name)
|
||||
|
||||
if !strings.HasPrefix(strings.ToLower(dir), c.base) {
|
||||
// We only validate things within the base directory, which we need to
|
||||
// compare case insensitively against.
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
// If we don't already have a list of directory entries for this
|
||||
// directory, try to list it. Return error if this fails.
|
||||
l, ok := c.results[dir]
|
||||
if !ok {
|
||||
if len(dir) > len(c.base) {
|
||||
// We are checking in a subdirectory of base. Must make sure *it*
|
||||
// exists with the specified casing, up to the base directory.
|
||||
if _, err := c.Lstat(dir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
l, err = fd.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Sort(fileInfoList(l))
|
||||
c.results[dir] = l
|
||||
}
|
||||
|
||||
// Get the index of the first entry with name >= base using binary search.
|
||||
idx := sort.Search(len(l), func(i int) bool {
|
||||
return l[i].Name() >= base
|
||||
})
|
||||
|
||||
if idx >= len(l) || l[idx].Name() != base {
|
||||
// The search didn't find any such entry
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
return l[idx], nil
|
||||
}
|
||||
|
||||
type fileInfoList []os.FileInfo
|
||||
|
||||
func (l fileInfoList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
func (l fileInfoList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
func (l fileInfoList) Less(a, b int) bool {
|
||||
return l[a].Name() < l[b].Name()
|
||||
}
|
||||
|
@ -7,8 +7,11 @@
|
||||
package osutil_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@ -179,3 +182,78 @@ func TestDiskUsage(t *testing.T) {
|
||||
t.Error("Disk is full?", free)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseSensitiveStat(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
break // We can test!
|
||||
default:
|
||||
t.Skip("Cannot test on this platform")
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir("", "TestCaseSensitiveStat")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(dir, "File"), []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(filepath.Join(dir, "File")); err != nil {
|
||||
// Standard Lstat should report the file exists
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(dir, "fILE")); err != nil {
|
||||
// ... also with the incorrect case spelling
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
// Create the case sensitive stat:er. We stress it a little by giving it a
|
||||
// base path with an intentionally incorrect casing.
|
||||
|
||||
css := osutil.NewCachedCaseSensitiveStat(strings.ToUpper(dir))
|
||||
|
||||
if _, err := css.Lstat(filepath.Join(dir, "File")); err != nil {
|
||||
// Our Lstat should report the file exists
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
if _, err := css.Lstat(filepath.Join(dir, "fILE")); err == nil || !os.IsNotExist(err) {
|
||||
// ... but with the incorrect case we should get ErrNotExist
|
||||
t.Fatal("Unexpected non-IsNotExist error:", err)
|
||||
}
|
||||
|
||||
// Now do the same tests for a file in a case-sensitive directory.
|
||||
|
||||
if err := os.Mkdir(filepath.Join(dir, "Dir"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(dir, "Dir/File"), []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(filepath.Join(dir, "Dir/File")); err != nil {
|
||||
// Standard Lstat should report the file exists
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(dir, "dIR/File")); err != nil {
|
||||
// ... also with the incorrect case spelling
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
// Recreate the case sensitive stat:er. We stress it a little by giving it a
|
||||
// base path with an intentionally incorrect casing.
|
||||
|
||||
css = osutil.NewCachedCaseSensitiveStat(strings.ToLower(dir))
|
||||
|
||||
if _, err := css.Lstat(filepath.Join(dir, "Dir/File")); err != nil {
|
||||
// Our Lstat should report the file exists
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
if _, err := css.Lstat(filepath.Join(dir, "dIR/File")); err == nil || !os.IsNotExist(err) {
|
||||
// ... but with the incorrect case we should get ErrNotExist
|
||||
t.Fatal("Unexpected non-IsNotExist error:", err)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user