Merge pull request #2337 from calmh/caseins

Case insensitive renames, part 1
This commit is contained in:
Audrius Butkevicius 2015-09-30 20:35:55 +01:00
commit 460cb19839
3 changed files with 168 additions and 1 deletions

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}
}