|
|
@@ -14,11 +14,14 @@ import (
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"time"
|
|
|
+
|
|
|
+ lru "github.com/hashicorp/golang-lru"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
// How long to consider cached dirnames valid
|
|
|
- caseCacheTimeout = time.Second
|
|
|
+ caseCacheTimeout = time.Second
|
|
|
+ caseCacheItemLimit = 4 << 10
|
|
|
)
|
|
|
|
|
|
type ErrCaseConflict struct {
|
|
|
@@ -351,96 +354,118 @@ func (f *caseFilesystem) checkCaseExisting(name string) error {
|
|
|
}
|
|
|
|
|
|
type defaultRealCaser struct {
|
|
|
- fs Filesystem
|
|
|
- root *caseNode
|
|
|
- mut sync.RWMutex
|
|
|
+ fs Filesystem
|
|
|
+ cache caseCache
|
|
|
}
|
|
|
|
|
|
func newDefaultRealCaser(fs Filesystem) *defaultRealCaser {
|
|
|
+ cache, err := lru.New2Q(caseCacheItemLimit)
|
|
|
+ // New2Q only errors if given invalid parameters, which we don't.
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
caser := &defaultRealCaser{
|
|
|
- fs: fs,
|
|
|
- root: &caseNode{name: "."},
|
|
|
+ fs: fs,
|
|
|
+ cache: caseCache{
|
|
|
+ TwoQueueCache: cache,
|
|
|
+ },
|
|
|
}
|
|
|
return caser
|
|
|
}
|
|
|
|
|
|
func (r *defaultRealCaser) realCase(name string) (string, error) {
|
|
|
- out := "."
|
|
|
- if name == out {
|
|
|
- return out, nil
|
|
|
+ realName := "."
|
|
|
+ if name == realName {
|
|
|
+ return realName, nil
|
|
|
}
|
|
|
|
|
|
- r.mut.Lock()
|
|
|
- defer r.mut.Unlock()
|
|
|
-
|
|
|
- node := r.root
|
|
|
for _, comp := range strings.Split(name, string(PathSeparator)) {
|
|
|
- if node.dirNames == nil || node.expires.Before(time.Now()) {
|
|
|
- // Haven't called DirNames yet, or the node has expired
|
|
|
+ node := r.cache.getExpireAdd(realName)
|
|
|
|
|
|
- var err error
|
|
|
- node.dirNames, err = r.fs.DirNames(out)
|
|
|
+ node.once.Do(func() {
|
|
|
+ dirNames, err := r.fs.DirNames(realName)
|
|
|
if err != nil {
|
|
|
- return "", err
|
|
|
+ r.cache.Remove(realName)
|
|
|
+ node.err = err
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- node.dirNamesLower = make([]string, len(node.dirNames))
|
|
|
- for i, n := range node.dirNames {
|
|
|
- node.dirNamesLower[i] = UnicodeLowercase(n)
|
|
|
+ num := len(dirNames)
|
|
|
+ node.children = make(map[string]struct{}, num)
|
|
|
+ node.lowerToReal = make(map[string]string, num)
|
|
|
+ lastLower := ""
|
|
|
+ for _, n := range dirNames {
|
|
|
+ node.children[n] = struct{}{}
|
|
|
+ lower := UnicodeLowercase(n)
|
|
|
+ if lower != lastLower {
|
|
|
+ node.lowerToReal[lower] = n
|
|
|
+ lastLower = n
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- node.expires = time.Now().Add(caseCacheTimeout)
|
|
|
- node.child = nil
|
|
|
+ })
|
|
|
+ if node.err != nil {
|
|
|
+ return "", node.err
|
|
|
}
|
|
|
|
|
|
- // If we don't already have a correct cached child, try to find it.
|
|
|
- if node.child == nil || node.child.name != comp {
|
|
|
- // Actually loop dirNames to search for a match.
|
|
|
- n, err := findCaseInsensitiveMatch(comp, node.dirNames, node.dirNamesLower)
|
|
|
- if err != nil {
|
|
|
- return "", err
|
|
|
+ // Try to find a direct or case match
|
|
|
+ if _, ok := node.children[comp]; !ok {
|
|
|
+ comp, ok = node.lowerToReal[UnicodeLowercase(comp)]
|
|
|
+ if !ok {
|
|
|
+ return "", ErrNotExist
|
|
|
}
|
|
|
- node.child = &caseNode{name: n}
|
|
|
}
|
|
|
|
|
|
- node = node.child
|
|
|
- out = filepath.Join(out, node.name)
|
|
|
+ realName = filepath.Join(realName, comp)
|
|
|
}
|
|
|
|
|
|
- return out, nil
|
|
|
+ return realName, nil
|
|
|
}
|
|
|
|
|
|
func (r *defaultRealCaser) dropCache() {
|
|
|
- r.mut.Lock()
|
|
|
- r.root = &caseNode{name: "."}
|
|
|
- r.mut.Unlock()
|
|
|
+ r.cache.Purge()
|
|
|
+}
|
|
|
+
|
|
|
+func newCaseNode() *caseNode {
|
|
|
+ return &caseNode{
|
|
|
+ expires: time.Now().Add(caseCacheTimeout),
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// Both name and the key to children are "Real", case resolved names of the path
|
|
|
+// The keys to children are "real", case resolved names of the path
|
|
|
// component this node represents (i.e. containing no path separator).
|
|
|
-// The key to results is also a path component, but as given to RealCase, not
|
|
|
-// case resolved.
|
|
|
+// lowerToReal is a map of lowercase path components (as in UnicodeLowercase)
|
|
|
+// to their corresponding "real", case resolved names.
|
|
|
+// A node is created empty and populated using once. If an error occurs the node
|
|
|
+// is removed from cache and the error stored in err, such that anyone that
|
|
|
+// already got the node doesn't try to access the nil maps.
|
|
|
type caseNode struct {
|
|
|
- name string
|
|
|
- expires time.Time
|
|
|
- dirNames []string
|
|
|
- dirNamesLower []string
|
|
|
- child *caseNode
|
|
|
-}
|
|
|
-
|
|
|
-func findCaseInsensitiveMatch(name string, names, namesLower []string) (string, error) {
|
|
|
- lower := UnicodeLowercase(name)
|
|
|
- candidate := ""
|
|
|
- for i, n := range names {
|
|
|
- if n == name {
|
|
|
- return n, nil
|
|
|
- }
|
|
|
- if candidate == "" && namesLower[i] == lower {
|
|
|
- candidate = n
|
|
|
- }
|
|
|
+ expires time.Time
|
|
|
+ lowerToReal map[string]string
|
|
|
+ children map[string]struct{}
|
|
|
+ once sync.Once
|
|
|
+ err error
|
|
|
+}
|
|
|
+
|
|
|
+type caseCache struct {
|
|
|
+ *lru.TwoQueueCache
|
|
|
+ mut sync.Mutex
|
|
|
+}
|
|
|
+
|
|
|
+// getExpireAdd gets an entry for the given key. If no entry exists, or it is
|
|
|
+// expired a new one is created and added to the cache.
|
|
|
+func (c *caseCache) getExpireAdd(key string) *caseNode {
|
|
|
+ c.mut.Lock()
|
|
|
+ defer c.mut.Unlock()
|
|
|
+ v, ok := c.Get(key)
|
|
|
+ if !ok {
|
|
|
+ node := newCaseNode()
|
|
|
+ c.Add(key, node)
|
|
|
+ return node
|
|
|
}
|
|
|
- if candidate == "" {
|
|
|
- return "", ErrNotExist
|
|
|
+ node := v.(*caseNode)
|
|
|
+ if node.expires.Before(time.Now()) {
|
|
|
+ node = newCaseNode()
|
|
|
+ c.Add(key, node)
|
|
|
}
|
|
|
- return candidate, nil
|
|
|
+ return node
|
|
|
}
|