diff options
Diffstat (limited to 'hugofs')
-rw-r--r-- | hugofs/glob/glob.go | 168 | ||||
-rw-r--r-- | hugofs/glob/glob_test.go | 35 |
2 files changed, 184 insertions, 19 deletions
diff --git a/hugofs/glob/glob.go b/hugofs/glob/glob.go index 57115ddfa..6dd0df5ed 100644 --- a/hugofs/glob/glob.go +++ b/hugofs/glob/glob.go @@ -16,6 +16,7 @@ package glob import ( "path" "path/filepath" + "runtime" "strings" "sync" @@ -23,46 +24,100 @@ import ( "github.com/gobwas/glob/syntax" ) +var ( + isWindows = runtime.GOOS == "windows" + defaultGlobCache = &globCache{ + isCaseSensitive: false, + isWindows: isWindows, + cache: make(map[string]globErr), + } + + filenamesGlobCache = &globCache{ + isCaseSensitive: true, // TODO(bep) bench + isWindows: isWindows, + cache: make(map[string]globErr), + } +) + type globErr struct { glob glob.Glob err error } -var ( - globCache = make(map[string]globErr) - globMu sync.RWMutex -) +type globCache struct { + // Config + isCaseSensitive bool + isWindows bool -type caseInsensitiveGlob struct { - g glob.Glob + // Cache + sync.RWMutex + cache map[string]globErr } -func (g caseInsensitiveGlob) Match(s string) bool { - return g.g.Match(strings.ToLower(s)) - -} -func GetGlob(pattern string) (glob.Glob, error) { +func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) { var eg globErr - globMu.RLock() + gc.RLock() var found bool - eg, found = globCache[pattern] - globMu.RUnlock() + eg, found = gc.cache[pattern] + gc.RUnlock() if found { return eg.glob, eg.err } + var g glob.Glob var err error - g, err := glob.Compile(strings.ToLower(pattern), '/') - eg = globErr{caseInsensitiveGlob{g: g}, err} - globMu.Lock() - globCache[pattern] = eg - globMu.Unlock() + pattern = filepath.ToSlash(pattern) + + if gc.isCaseSensitive { + g, err = glob.Compile(pattern, '/') + } else { + g, err = glob.Compile(strings.ToLower(pattern), '/') + + } + + eg = globErr{ + globDecorator{ + g: g, + isCaseSensitive: gc.isCaseSensitive, + isWindows: gc.isWindows}, + err, + } + + gc.Lock() + gc.cache[pattern] = eg + gc.Unlock() return eg.glob, eg.err } +type globDecorator struct { + // Whether both pattern and the strings to match will be matched + // by their original case. + isCaseSensitive bool + + // On Windows we may get filenames with Windows slashes to match, + // which wee need to normalize. + isWindows bool + + g glob.Glob +} + +func (g globDecorator) Match(s string) bool { + if g.isWindows { + s = filepath.ToSlash(s) + } + if !g.isCaseSensitive { + s = strings.ToLower(s) + } + return g.g.Match(s) +} + +func GetGlob(pattern string) (glob.Glob, error) { + return defaultGlobCache.GetGlob(pattern) +} + func NormalizePath(p string) string { return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.") } @@ -106,3 +161,78 @@ func HasGlobChar(s string) bool { } return false } + +type FilenameFilter struct { + shouldInclude func(filename string) bool + inclusions []glob.Glob + exclusions []glob.Glob + isWindows bool +} + +// NewFilenameFilter creates a new Glob where the Match method will +// return true if the file should be exluded. +// Note that the inclusions will be checked first. +func NewFilenameFilter(inclusions, exclusions []string) (*FilenameFilter, error) { + filter := &FilenameFilter{isWindows: isWindows} + + for _, include := range inclusions { + g, err := filenamesGlobCache.GetGlob(filepath.FromSlash(include)) + if err != nil { + return nil, err + } + filter.inclusions = append(filter.inclusions, g) + } + for _, exclude := range exclusions { + g, err := filenamesGlobCache.GetGlob(filepath.FromSlash(exclude)) + if err != nil { + return nil, err + } + filter.exclusions = append(filter.exclusions, g) + } + + return filter, nil +} + +// NewFilenameFilterForInclusionFunc create a new filter using the provided inclusion func. +func NewFilenameFilterForInclusionFunc(shouldInclude func(filename string) bool) *FilenameFilter { + return &FilenameFilter{shouldInclude: shouldInclude, isWindows: isWindows} +} + +// Match returns whether filename should be included. +func (f *FilenameFilter) Match(filename string) bool { + if f == nil { + return true + } + + if f.shouldInclude != nil { + if f.shouldInclude(filename) { + return true + } + if f.isWindows { + // The Glob matchers below handles this by themselves, + // for the shouldInclude we need to take some extra steps + // to make this robust. + winFilename := filepath.FromSlash(filename) + if filename != winFilename { + if f.shouldInclude(winFilename) { + return true + } + } + } + + } + + for _, inclusion := range f.inclusions { + if inclusion.Match(filename) { + return true + } + } + + for _, exclusion := range f.exclusions { + if exclusion.Match(filename) { + return false + } + } + + return f.inclusions == nil && f.shouldInclude == nil +} diff --git a/hugofs/glob/glob_test.go b/hugofs/glob/glob_test.go index cd64ba112..7ef3fbbed 100644 --- a/hugofs/glob/glob_test.go +++ b/hugofs/glob/glob_test.go @@ -15,6 +15,7 @@ package glob import ( "path/filepath" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -72,6 +73,40 @@ func TestGetGlob(t *testing.T) { c.Assert(g.Match("data/my.json"), qt.Equals, true) } +func TestFilenameFilter(t *testing.T) { + c := qt.New(t) + + excludeAlmostAllJSON, err := NewFilenameFilter([]string{"a/b/c/foo.json"}, []string{"**.json"}) + c.Assert(err, qt.IsNil) + c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("data/my.json")), qt.Equals, false) + c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("a/b/c/foo.json")), qt.Equals, true) + c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("a/b/c/foo.bar")), qt.Equals, false) + + nopFilter, err := NewFilenameFilter(nil, nil) + c.Assert(err, qt.IsNil) + c.Assert(nopFilter.Match("ab.txt"), qt.Equals, true) + + includeOnlyFilter, err := NewFilenameFilter([]string{"**.json", "**.jpg"}, nil) + c.Assert(err, qt.IsNil) + c.Assert(includeOnlyFilter.Match("ab.json"), qt.Equals, true) + c.Assert(includeOnlyFilter.Match("ab.jpg"), qt.Equals, true) + c.Assert(includeOnlyFilter.Match("ab.gif"), qt.Equals, false) + + exlcudeOnlyFilter, err := NewFilenameFilter(nil, []string{"**.json", "**.jpg"}) + c.Assert(err, qt.IsNil) + c.Assert(exlcudeOnlyFilter.Match("ab.json"), qt.Equals, false) + c.Assert(exlcudeOnlyFilter.Match("ab.jpg"), qt.Equals, false) + c.Assert(exlcudeOnlyFilter.Match("ab.gif"), qt.Equals, true) + + var nilFilter *FilenameFilter + c.Assert(nilFilter.Match("ab.gif"), qt.Equals, true) + + funcFilter := NewFilenameFilterForInclusionFunc(func(s string) bool { return strings.HasSuffix(s, ".json") }) + c.Assert(funcFilter.Match("ab.json"), qt.Equals, true) + c.Assert(funcFilter.Match("ab.bson"), qt.Equals, false) + +} + func BenchmarkGetGlob(b *testing.B) { for i := 0; i < b.N; i++ { _, err := GetGlob("**/foo") |