mirror of
https://github.com/octoleo/syncthing.git
synced 2025-02-11 00:08:38 +00: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
|
// TODO: We should limit the Have scanning to start at sub
|
||||||
seenPrefix := false
|
seenPrefix := false
|
||||||
var iterError error
|
var iterError error
|
||||||
|
css := osutil.NewCachedCaseSensitiveStat(folderCfg.Path())
|
||||||
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||||
f := fi.(db.FileInfoTruncated)
|
f := fi.(db.FileInfoTruncated)
|
||||||
hasPrefix := len(subs) == 0
|
hasPrefix := len(subs) == 0
|
||||||
@ -1413,7 +1414,7 @@ nextSub:
|
|||||||
Version: f.Version, // The file is still the same, so don't bump version
|
Version: f.Version, // The file is still the same, so don't bump version
|
||||||
}
|
}
|
||||||
batch = append(batch, nf)
|
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.
|
// File has been deleted.
|
||||||
|
|
||||||
// We don't specifically verify that the error is
|
// We don't specifically verify that the error is
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -241,3 +242,90 @@ func SetTCPOptions(conn *net.TCPConn) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
package osutil_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
@ -179,3 +182,78 @@ func TestDiskUsage(t *testing.T) {
|
|||||||
t.Error("Disk is full?", free)
|
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…
x
Reference in New Issue
Block a user