From 904296da2f93b1376d7e2f5395ad11053ec7ab59 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Tue, 3 Dec 2024 00:17:34 +0700 Subject: lib/memfs: move compiled regex to their options This changes move the following fields and methods from MemFS, * incRE and excRE fields to Options, * isIncluded and isExclude methods to Options, * watchRE field to WatchOptions, * isWatched method to WatchOptions. The reason is to allow other type that use Options or WatchOptions to use isIncluded, isExclude, or isWatched methods. --- lib/memfs/memfs.go | 116 +++++------------------------- lib/memfs/memfs_test.go | 147 -------------------------------------- lib/memfs/options.go | 70 +++++++++++++++++- lib/memfs/options_test.go | 174 +++++++++++++++++++++++++++++++++++++++++++++ lib/memfs/watch_options.go | 38 +++++++++- 5 files changed, 295 insertions(+), 250 deletions(-) create mode 100644 lib/memfs/options_test.go diff --git a/lib/memfs/memfs.go b/lib/memfs/memfs.go index 3d90a8b0..9b3ec668 100644 --- a/lib/memfs/memfs.go +++ b/lib/memfs/memfs.go @@ -13,7 +13,6 @@ import ( "net/http" "os" "path/filepath" - "regexp" "sort" "strings" "time" @@ -36,10 +35,7 @@ type MemFS struct { Root *Node Opts *Options dw *DirWatcher - - watchRE []*regexp.Regexp - incRE []*regexp.Regexp - excRE []*regexp.Regexp + watchopts *WatchOptions // subfs contains another MemFS instances. // During Get, it will evaluated in order. @@ -75,10 +71,10 @@ func (mfs *MemFS) AddChild(parent *Node, fi os.FileInfo) (child *Node, err error sysPath = filepath.Join(parent.SysPath, fi.Name()) ) - if mfs.isExcluded(sysPath) { + if mfs.Opts.isExcluded(sysPath) { return nil, nil } - if mfs.isWatched(sysPath) { + if mfs.watchopts != nil && mfs.watchopts.isWatched(sysPath) { child, err = parent.addChild(sysPath, fi, mfs.Opts.MaxFileSize) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -89,7 +85,7 @@ func (mfs *MemFS) AddChild(parent *Node, fi os.FileInfo) (child *Node, err error mfs.PathNodes.Set(child.Path, child) } - if !mfs.isIncluded(sysPath, fi) { + if !mfs.Opts.isIncluded(sysPath, fi) { if child != nil { // The path being watched, but not included. // Set the generate function name to empty, to prevent @@ -268,34 +264,18 @@ func (mfs *MemFS) Get(path string) (node *Node, err error) { // This method provided to initialize MemFS if its Options is set directly, // not through New() function. func (mfs *MemFS) Init() (err error) { - var ( - logp = "Init" - v string - re *regexp.Regexp - ) + var logp = `Init` if mfs.Opts == nil { mfs.Opts = &Options{} } - mfs.Opts.init() - if mfs.PathNodes == nil { mfs.PathNodes = NewPathNode() } - for _, v = range mfs.Opts.Includes { - re, err = regexp.Compile(v) - if err != nil { - return fmt.Errorf("%s: %w", logp, err) - } - mfs.incRE = append(mfs.incRE, re) - } - for _, v = range mfs.Opts.Excludes { - re, err = regexp.Compile(v) - if err != nil { - return fmt.Errorf("%s: %w", logp, err) - } - mfs.excRE = append(mfs.excRE, re) + err = mfs.Opts.init() + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) } err = mfs.mount() @@ -471,62 +451,6 @@ func (mfs *MemFS) createRoot() error { return nil } -// isExcluded will return true if the system path is excluded from being -// watched or included. -func (mfs *MemFS) isExcluded(sysPath string) bool { - var ( - re *regexp.Regexp - ) - for _, re = range mfs.excRE { - if re.MatchString(sysPath) { - return true - } - } - return false -} - -// isIncluded will return true if the system path is filtered to be included, -// pass the list of Includes regexp or no filter defined. -func (mfs *MemFS) isIncluded(sysPath string, fi os.FileInfo) bool { - var ( - re *regexp.Regexp - err error - ) - - if len(mfs.incRE) == 0 { - // No filter defined, default to always included. - return true - } - for _, re = range mfs.incRE { - if re.MatchString(sysPath) { - return true - } - } - if fi.Mode()&os.ModeSymlink == os.ModeSymlink { - // File is symlink, get the real FileInfo to check if its - // directory or not. - fi, err = os.Stat(sysPath) - if err != nil { - return false - } - } - - return fi.IsDir() -} - -// isWatched will return true if the system path is filtered to be watched. -func (mfs *MemFS) isWatched(sysPath string) bool { - var ( - re *regexp.Regexp - ) - for _, re = range mfs.watchRE { - if re.MatchString(sysPath) { - return true - } - } - return false -} - // mount the directory recursively into the memory as root directory. // For example, if we mount directory "/tmp" and "/tmp" contains file "a", to // access file "a" we call Get("/a"), not Get("/tmp/a"). @@ -700,30 +624,22 @@ func (mfs *MemFS) resetAllModTime(t time.Time) { // // The returned DirWatcher is ready to use. // To stop watching for update call the StopWatch. -func (mfs *MemFS) Watch(opts WatchOptions) (dw *DirWatcher, err error) { - var ( - logp = "Watch" +func (mfs *MemFS) Watch(watchopts WatchOptions) (dw *DirWatcher, err error) { + var logp = `Watch` - re *regexp.Regexp - v string - ) + err = watchopts.init() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + mfs.watchopts = &watchopts if mfs.dw != nil { return mfs.dw, nil } - mfs.watchRE = nil - for _, v = range opts.Watches { - re, err = regexp.Compile(v) - if err != nil { - return nil, fmt.Errorf("%s: %w", logp, err) - } - mfs.watchRE = append(mfs.watchRE, re) - } - mfs.dw = &DirWatcher{ fs: mfs, - Delay: opts.Delay, + Delay: watchopts.Delay, Options: *mfs.Opts, } diff --git a/lib/memfs/memfs_test.go b/lib/memfs/memfs_test.go index 9b82121b..fb97ae1b 100644 --- a/lib/memfs/memfs_test.go +++ b/lib/memfs/memfs_test.go @@ -705,153 +705,6 @@ func TestMemFS_RemoveChild(t *testing.T) { } } -func TestMemFS_isIncluded(t *testing.T) { - cases := []struct { - desc string - inc []string - exc []string - sysPath []string - exp []bool - }{{ - desc: "With empty includes and excludes", - sysPath: []string{ - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/index.html"), - }, - exp: []bool{ - true, - true, - }, - }, { - desc: "With excludes only", - exc: []string{ - `.*/exclude`, - `.*\.html$`, - }, - sysPath: []string{ - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/exclude"), - filepath.Join(_testWD, "/testdata/exclude/dir"), - filepath.Join(_testWD, "/testdata/include"), - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/index.html"), - filepath.Join(_testWD, "/testdata/index.css"), - }, - exp: []bool{ - true, - false, - false, - true, - true, - false, - true, - }, - }, { - desc: "With includes only", - inc: []string{ - ".*/include", - `.*\.html$`, - }, - sysPath: []string{ - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/include"), - filepath.Join(_testWD, "/testdata/include/dir"), - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/index.html"), - filepath.Join(_testWD, "/testdata/index.css"), - }, - exp: []bool{ - true, - true, - true, - true, - true, - false, - }, - }, { - desc: "With excludes and includes", - exc: []string{ - `.*/exclude`, - `.*\.js$`, - }, - inc: []string{ - `.*/include`, - `.*\.(css|html)$`, - }, - sysPath: []string{ - filepath.Join(_testWD, "/testdata"), - filepath.Join(_testWD, "/testdata/index.html"), - filepath.Join(_testWD, "/testdata/index.css"), - - filepath.Join(_testWD, "/testdata/exclude"), - filepath.Join(_testWD, "/testdata/exclude/dir"), - filepath.Join(_testWD, "/testdata/exclude/index-link.css"), - filepath.Join(_testWD, "/testdata/exclude/index-link.html"), - filepath.Join(_testWD, "/testdata/exclude/index-link.js"), - - filepath.Join(_testWD, "/testdata/include"), - filepath.Join(_testWD, "/testdata/include/dir"), - filepath.Join(_testWD, "/testdata/include/index.css"), - filepath.Join(_testWD, "/testdata/include/index.html"), - filepath.Join(_testWD, "/testdata/include/index.js"), - }, - exp: []bool{ - true, - true, - true, - - false, - false, - false, - false, - false, - - true, - true, - true, - true, - false, - }, - }} - - var ( - opts *Options - mfs *MemFS - fi os.FileInfo - sysPath string - err error - x int - got bool - ) - for _, c := range cases { - t.Log(c.desc) - - opts = &Options{ - Includes: c.inc, - Excludes: c.exc, - } - mfs, err = New(opts) - if err != nil { - t.Fatal(err) - } - - for x, sysPath = range c.sysPath { - fi, err = os.Stat(sysPath) - if err != nil { - t.Fatal(err) - } - - got = mfs.isExcluded(sysPath) - if got { - test.Assert(t, sysPath, !c.exp[x], got) - } else { - got = mfs.isIncluded(sysPath, fi) - test.Assert(t, sysPath, c.exp[x], got) - } - } - } -} - func TestScanDir(t *testing.T) { opts := Options{ Root: "testdata/", diff --git a/lib/memfs/options.go b/lib/memfs/options.go index 0b415f5c..3f304f0c 100644 --- a/lib/memfs/options.go +++ b/lib/memfs/options.go @@ -4,7 +4,11 @@ package memfs -import "strings" +import ( + "os" + "regexp" + "strings" +) const ( defaultMaxFileSize = 1024 * 1024 * 5 @@ -27,6 +31,9 @@ type Options struct { Includes []string Excludes []string + incRE []*regexp.Regexp + excRE []*regexp.Regexp + // MaxFileSize define maximum file size that can be stored on memory. // The default value is 5 MB. // If its value is negative, the content of file will not be mapped to @@ -46,12 +53,71 @@ type Options struct { } // init initialize the options with default value. -func (opts *Options) init() { +func (opts *Options) init() (err error) { if opts.MaxFileSize == 0 { opts.MaxFileSize = defaultMaxFileSize } + opts.Root = strings.TrimSuffix(opts.Root, `/`) if len(opts.Root) == 0 { opts.Root = `.` } + + var ( + v string + re *regexp.Regexp + ) + for _, v = range opts.Includes { + re, err = regexp.Compile(v) + if err != nil { + return err + } + opts.incRE = append(opts.incRE, re) + } + for _, v = range opts.Excludes { + re, err = regexp.Compile(v) + if err != nil { + return err + } + opts.excRE = append(opts.excRE, re) + } + return nil +} + +// isExcluded return true if the sysPath is match with one of regex in +// Excludes. +func (opts *Options) isExcluded(sysPath string) bool { + var re *regexp.Regexp + for _, re = range opts.excRE { + if re.MatchString(sysPath) { + return true + } + } + return false +} + +// isIncluded return true if the sysPath is pass the list of Includes +// regexp, or no filter defined. +func (opts *Options) isIncluded(sysPath string, fi os.FileInfo) bool { + if len(opts.incRE) == 0 { + // No filter defined, default to always included. + return true + } + var re *regexp.Regexp + for _, re = range opts.incRE { + if re.MatchString(sysPath) { + return true + } + } + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + // File is symlink, get the real FileInfo to check if its + // directory or not. + var err error + fi, err = os.Stat(sysPath) + if err != nil { + return false + } + } + + return fi.IsDir() } diff --git a/lib/memfs/options_test.go b/lib/memfs/options_test.go new file mode 100644 index 00000000..6dcb14a7 --- /dev/null +++ b/lib/memfs/options_test.go @@ -0,0 +1,174 @@ +// Copyright 2024, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +import ( + "os" + "path/filepath" + "testing" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestOptionsIsIncluded(t *testing.T) { + type testData struct { + sysPath string + exp bool + } + + var cases = []struct { + data []testData + desc string + opts Options + }{{ + desc: `With empty includes and excludes`, + data: []testData{{ + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.html`), + exp: true, + }}, + }, { + desc: `With excludes only`, + opts: Options{ + Excludes: []string{ + `.*/exclude`, + `.*\.html$`, + }, + }, + data: []testData{{ + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude/dir`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.html`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.css`), + exp: true, + }}, + }, { + desc: `With includes only`, + opts: Options{ + Includes: []string{ + `.*/include`, + `.*\.html$`, + }, + }, + data: []testData{{ + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include/dir`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.html`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.css`), + exp: false, + }}, + }, { + desc: `With excludes and includes`, + opts: Options{ + Excludes: []string{ + `.*/exclude`, + `.*\.js$`, + }, + Includes: []string{ + `.*/include`, + `.*\.(css|html)$`, + }, + }, + data: []testData{{ + sysPath: filepath.Join(_testWD, `/testdata`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.html`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/index.css`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude/dir`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude/index-link.css`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude/index-link.html`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/exclude/index-link.js`), + exp: false, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include/dir`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include/index.css`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include/index.html`), + exp: true, + }, { + sysPath: filepath.Join(_testWD, `/testdata/include/index.js`), + exp: false, + }}, + }} + + var ( + fi os.FileInfo + tdata testData + err error + got bool + ) + for _, c := range cases { + t.Log(c.desc) + + err = c.opts.init() + if err != nil { + t.Fatal(err) + } + + for _, tdata = range c.data { + fi, err = os.Stat(tdata.sysPath) + if err != nil { + t.Fatal(err) + } + + got = c.opts.isExcluded(tdata.sysPath) + if got { + test.Assert(t, tdata.sysPath, !tdata.exp, got) + } else { + got = c.opts.isIncluded(tdata.sysPath, fi) + test.Assert(t, tdata.sysPath, tdata.exp, got) + } + } + } +} diff --git a/lib/memfs/watch_options.go b/lib/memfs/watch_options.go index 119ea145..412e8843 100644 --- a/lib/memfs/watch_options.go +++ b/lib/memfs/watch_options.go @@ -4,7 +4,10 @@ package memfs -import "time" +import ( + "regexp" + "time" +) // WatchOptions define an options for the MemFS Watch method. type WatchOptions struct { @@ -14,9 +17,42 @@ type WatchOptions struct { // watched. Watches []string + watchRE []*regexp.Regexp + // Delay define the duration when the new changes will be checked from // system. // This field set the DirWatcher.Delay returned from Watch(). // This field is optional, default is 5 seconds. Delay time.Duration } + +func (watchopts *WatchOptions) init() (err error) { + if watchopts.Delay < 100*time.Millisecond { + watchopts.Delay = defWatchDelay + } + + var ( + v string + re *regexp.Regexp + ) + watchopts.watchRE = nil + for _, v = range watchopts.Watches { + re, err = regexp.Compile(v) + if err != nil { + return err + } + watchopts.watchRE = append(watchopts.watchRE, re) + } + return nil +} + +// isWatched return true if the sysPath is filtered to be watched. +func (watchopts *WatchOptions) isWatched(sysPath string) bool { + var re *regexp.Regexp + for _, re = range watchopts.watchRE { + if re.MatchString(sysPath) { + return true + } + } + return false +} -- cgit v1.3