diff options
| author | Shulhan <ms@kilabit.info> | 2024-12-16 01:01:24 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-12-28 16:26:40 +0700 |
| commit | c14c0e7735778573dc4d87e39703f0524d08a1b0 (patch) | |
| tree | 6e4e5c30584dbbc986063dcc754162ec778147d5 | |
| parent | 5f97e1d495dd341f2bf573d80b0edb4d02a78a7b (diff) | |
| download | pakakeh.go-c14c0e7735778573dc4d87e39703f0524d08a1b0.tar.xz | |
watchfs/v2: implement new directory watcher
DirWatcher scan the content of directory in [fs.DirWatcherOptions.Root]
recursively for the files to be watched, using the
[fs.DirWatcherOptions.Includes] field.
A single file, [fs.DirWatcherOptions.FileWatcherOptions.FilePath], will
be watched for changes that will trigger re-scanning the content of Root
recursively.
The result of re-scanning is list of the Includes files (only files not
new directory) that are changes, which will be send to channel C.
On each [os.FileInfo] received from C, a deleted file have
[os.FileInfo.Size] equal to [NodeFlagDeleted].
The channel will send an empty slice if no changes.
The implementation of file changes in this code is naive, using loop and
comparison of mode, modification time, and size; at least it should
works on most operating system.
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | lib/watchfs/v2/dir_watcher.go | 306 | ||||
| -rw-r--r-- | lib/watchfs/v2/dir_watcher_example_test.go | 82 | ||||
| -rw-r--r-- | lib/watchfs/v2/dir_watcher_options.go | 86 | ||||
| -rw-r--r-- | lib/watchfs/v2/dir_watcher_test.go | 296 | ||||
| -rw-r--r-- | lib/watchfs/v2/node.go | 92 | ||||
| -rw-r--r-- | lib/watchfs/v2/testdata/exc/index.adoc | 0 | ||||
| -rw-r--r-- | lib/watchfs/v2/testdata/inc/index.adoc | 1 | ||||
| -rw-r--r-- | lib/watchfs/v2/testdata/inc/index.css | 0 | ||||
| -rw-r--r-- | lib/watchfs/v2/testdata/inc/index.html | 0 | ||||
| -rw-r--r-- | lib/watchfs/v2/watchfs.go | 2 |
11 files changed, 865 insertions, 2 deletions
@@ -4,7 +4,6 @@ *.cprof *.csv *.dat -*.html *.mprof *.oob *.perf @@ -15,6 +14,7 @@ *.stats *.test *.zst +/*.html /_bin/ansua /_bin/bcrypt /_bin/emaildecode diff --git a/lib/watchfs/v2/dir_watcher.go b/lib/watchfs/v2/dir_watcher.go new file mode 100644 index 00000000..0cbef3b1 --- /dev/null +++ b/lib/watchfs/v2/dir_watcher.go @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: BSD-3-Clause + +package watchfs + +import ( + "errors" + "fmt" + "maps" + "os" + "path/filepath" + "slices" + "time" +) + +// DirWatcher scan the content of directory in +// [watchfs.DirWatcherOptions.Root] recursively for the files to be watched, +// using the [watchfs.DirWatcherOptions.Includes] field. +// A single file, [watchfs.DirWatcherOptions.File], will be watched for +// changes that will trigger re-scanning the content of Root recursively. +// +// The result of re-scanning is list of the Includes files (only files not +// new directory) that are changes, which will be send to channel C. +// On each [os.FileInfo] received from C, a deleted file have +// [os.FileInfo.Size] equal to [FileFlagDeleted]. +// The channel will send an empty slice if no changes. +// +// The implementation of file changes in this code is naive, using loop and +// comparison of mode, modification time, and size; at least it should works +// on most operating system. +type DirWatcher struct { + // idxDir contains index of directory. + // It is used to detect new or deleted file inside that directory. + idxDir map[string]node + + // idxFile contains index of files. + idxFile map[string]node + + idxNewFile map[string]node + + // C received the new, updated, and deleted files. + C <-chan []os.FileInfo + c chan []os.FileInfo + + fwatch *FileWatcher + opts DirWatcherOptions +} + +// WatchDir create and start scanning directory for changes. +func WatchDir(opts DirWatcherOptions) (dwatch *DirWatcher, err error) { + err = opts.init() + if err != nil { + return nil, fmt.Errorf(`WatchDir: %w`, err) + } + + dwatch = &DirWatcher{ + c: make(chan []os.FileInfo, 1), + opts: opts, + idxDir: map[string]node{}, + idxFile: map[string]node{}, + idxNewFile: map[string]node{}, + } + dwatch.C = dwatch.c + dwatch.initialScan(dwatch.opts.Root) + dwatch.fwatch = WatchFile(dwatch.opts.FileWatcherOptions) + go dwatch.watch() + return dwatch, nil +} + +// Files return all the files currently being watched, the one that filtered +// by [watchfs.DirWatcherOptions.Includes], with its file information. +// This method is not safe when called when DirWatcher has been running. +func (dwatch *DirWatcher) Files() (files map[string]os.FileInfo) { + files = make(map[string]os.FileInfo) + for key, node := range dwatch.idxFile { + if node.size == nodeFlagExcluded { + continue + } + files[key] = &node + } + return files +} + +// ForceRescan force to rescan for changes without waiting for +// [watchfs.DirWatcherOptions.File] to be updated. +func (dwatch *DirWatcher) ForceRescan() { + if dwatch.fwatch != nil { + dwatch.fwatch.c <- &node{ + name: `.watchfs_v2_forced`, + size: nodeFlagForced, + mtime: time.Now(), + } + } +} + +// Stop watching the file and re-scanning the Root directory. +func (dwatch *DirWatcher) Stop() { + if dwatch.fwatch != nil { + dwatch.fwatch.Stop() + } +} + +func (dwatch *DirWatcher) indexingFile(apath string) (anode *node) { + if dwatch.opts.isExcluded(apath) { + return &nodeExcluded + } + if !dwatch.opts.isIncluded(apath) { + return &nodeExcluded + } + if apath == dwatch.opts.FileWatcherOptions.File { + return &nodeExcluded + } + anode, _ = newNode(apath) + // The newNode may return nil, so we will + // let the next re-scan to include them + // later. + return anode +} + +// initialScan scan the directory dir and its sub-directories to get the +// list of directory and the list of included files. +func (dwatch *DirWatcher) initialScan(dir string) (changes []os.FileInfo) { + var ( + dirq = []string{dir} + name string + apath string + anode *node + listde []os.DirEntry + err error + de os.DirEntry + ) + for len(dirq) > 0 { + dir = dirq[0] + dirq = dirq[1:] + + anode, err = newNode(dir) + if err != nil { + continue + } + if dwatch.opts.isExcluded(dir) { + dwatch.idxDir[dir] = nodeExcluded + continue + } + + listde, err = os.ReadDir(dir) + if err != nil { + continue + } + anode.size = int64(len(listde)) + dwatch.idxDir[dir] = *anode + + for _, de = range listde { + name = de.Name() + apath = filepath.Join(dir, name) + if de.IsDir() { + dirq = append(dirq, apath) + continue + } + anode = dwatch.indexingFile(apath) + if anode != nil { + dwatch.idxFile[apath] = *anode + changes = append(changes, anode) + } + } + } + return changes +} + +func (dwatch *DirWatcher) scanDir() (changes []os.FileInfo) { + // Fetch the current keys first, so in case there is new directory + // it does not included twice. + var keys = slices.Sorted(maps.Keys(dwatch.idxDir)) + var ( + dir string + anode node + newnode *node + err error + listde []os.DirEntry + ) + for _, dir = range keys { + anode = dwatch.idxDir[dir] + if anode.size == nodeFlagExcluded { + continue + } + + newnode, err = newNode(dir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // The directory has been deleted. + anode = node{ + name: dir, + size: FileFlagDeleted, + mode: os.ModeDir, + } + changes = append(changes, &anode) + delete(dwatch.idxDir, dir) + } + continue + } + + listde, err = os.ReadDir(dir) + if err != nil { + continue + } + newnode.size = int64(len(listde)) + + if anode.equal(newnode) { + continue + } + dwatch.idxDir[dir] = *newnode + + var ( + de os.DirEntry + name string + apath string + ) + for _, de = range listde { + name = de.Name() + apath = filepath.Join(dir, name) + + if de.IsDir() { + anode = dwatch.idxDir[apath] + if anode.mtime.IsZero() { + // New directory created. + var newFiles = dwatch.initialScan(apath) + changes = append(changes, newFiles...) + } + // anode is a directory that has been + // indexed. + continue + } + + anode = dwatch.idxFile[apath] + if anode.mtime.IsZero() { + // New file created. + newnode = dwatch.indexingFile(apath) + if newnode != nil { + dwatch.idxNewFile[apath] = *newnode + } + continue + } + } + } + return changes +} + +func (dwatch *DirWatcher) scanFile() (fileChanges []os.FileInfo) { + var ( + newnode *node + err error + ) + for apath, anode := range dwatch.idxFile { + if anode.size == nodeFlagExcluded { + continue + } + newnode, err = newNode(apath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // File has been deleted. + newnode = &node{ + name: apath, + size: FileFlagDeleted, + } + fileChanges = append(fileChanges, newnode) + delete(dwatch.idxFile, apath) + } + continue + } + if anode.equal(newnode) { + continue + } + // File has been updated. + dwatch.idxFile[apath] = *newnode + fileChanges = append(fileChanges, newnode) + } + return fileChanges +} + +func (dwatch *DirWatcher) watch() { + var ( + dirChanges []os.FileInfo + fileChanges []os.FileInfo + ) + for range dwatch.fwatch.C { + // Scan new files on each directory. + dirChanges = dwatch.scanDir() + + // Scan update or delete files. + fileChanges = dwatch.scanFile() + + if len(dwatch.idxNewFile) != 0 { + for apath, anode := range dwatch.idxNewFile { + dwatch.idxFile[apath] = anode + if anode.size == nodeFlagExcluded { + continue + } + fileChanges = append(fileChanges, &anode) + } + clear(dwatch.idxNewFile) + } + + dirChanges = append(dirChanges, fileChanges...) + dwatch.c <- dirChanges + } + close(dwatch.c) +} diff --git a/lib/watchfs/v2/dir_watcher_example_test.go b/lib/watchfs/v2/dir_watcher_example_test.go new file mode 100644 index 00000000..95cce46f --- /dev/null +++ b/lib/watchfs/v2/dir_watcher_example_test.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: BSD-3-Clause + +package watchfs_test + +import ( + "fmt" + "log" + "os" + "path/filepath" + "sort" + "time" + + "git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2" +) + +func ExampleWatchDir() { + var ( + dirTemp string + err error + ) + dirTemp, err = os.MkdirTemp(``, ``) + if err != nil { + log.Fatal(err) + } + + var ( + fileToWatch = filepath.Join(dirTemp, `.rescan`) + opts = watchfs.DirWatcherOptions{ + FileWatcherOptions: watchfs.FileWatcherOptions{ + File: fileToWatch, + Interval: 50 * time.Millisecond, + }, + Root: dirTemp, + Includes: []string{`.*\.adoc$`}, + Excludes: []string{`exc$`, `.*\.html$`}, + } + dwatch *watchfs.DirWatcher + ) + + dwatch, err = watchfs.WatchDir(opts) + if err != nil { + log.Fatal(err) + } + + var ( + fileAadoc = filepath.Join(opts.Root, `a.adoc`) + fileBadoc = filepath.Join(opts.Root, `b.adoc`) + fileAhtml = filepath.Join(opts.Root, `a.html`) + ) + + err = os.WriteFile(fileAadoc, nil, 0600) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile(fileAhtml, nil, 0600) + if err != nil { + log.Fatal(err) + } + err = os.WriteFile(fileBadoc, nil, 0600) + if err != nil { + log.Fatal(err) + } + + // Write to the file that we watch for changes to trigger rescan. + err = os.WriteFile(fileToWatch, []byte(`x`), 0600) + if err != nil { + log.Fatal(err) + } + + var changes []os.FileInfo = <-dwatch.C + var names []string + for _, fi := range changes { + // Since we use temporary directory, print only the base + // name to make it works on all system. + names = append(names, filepath.Base(fi.Name())) + } + sort.Strings(names) + fmt.Println(names) + // Output: + // [a.adoc b.adoc] +} diff --git a/lib/watchfs/v2/dir_watcher_options.go b/lib/watchfs/v2/dir_watcher_options.go new file mode 100644 index 00000000..6957129c --- /dev/null +++ b/lib/watchfs/v2/dir_watcher_options.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: BSD-3-Clause + +package watchfs + +import ( + "regexp" + "strings" +) + +// DirWatcherOptions contains options to watch directory. +type DirWatcherOptions struct { + FileWatcherOptions + + // The root directory where files to be scanned. + Root string + + // List of regex for files or directories to be excluded from + // scanning. + // The Excludes option will be processed before Includes. + Excludes []string + + // List of regex for files or directories to be included from + // scanning. + Includes []string + + reExcludes []*regexp.Regexp + reIncludes []*regexp.Regexp +} + +func (opts *DirWatcherOptions) init() (err error) { + var ( + str string + re *regexp.Regexp + ) + for _, str = range opts.Excludes { + str = strings.TrimSpace(str) + if len(str) == 0 { + // Accidentally using empty string here may cause + // everying get excluded. + continue + } + re, err = regexp.Compile(str) + if err != nil { + return err + } + opts.reExcludes = append(opts.reExcludes, re) + } + for _, str = range opts.Includes { + str = strings.TrimSpace(str) + if len(str) == 0 { + continue + } + re, err = regexp.Compile(str) + if err != nil { + return err + } + opts.reIncludes = append(opts.reIncludes, re) + } + return nil +} + +func (opts *DirWatcherOptions) isExcluded(pathFile string) bool { + var re *regexp.Regexp + for _, re = range opts.reExcludes { + if re.MatchString(pathFile) { + return true + } + } + return false +} + +// isIncluded will return true if the list Includes is empty or it is match +// with one of the Includes regex. +func (opts *DirWatcherOptions) isIncluded(pathFile string) bool { + if len(opts.reIncludes) == 0 { + return true + } + var re *regexp.Regexp + for _, re = range opts.reIncludes { + if re.MatchString(pathFile) { + return true + } + } + return false +} diff --git a/lib/watchfs/v2/dir_watcher_test.go b/lib/watchfs/v2/dir_watcher_test.go new file mode 100644 index 00000000..2f0552e3 --- /dev/null +++ b/lib/watchfs/v2/dir_watcher_test.go @@ -0,0 +1,296 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: BSD-3-Clause + +package watchfs + +import ( + "io/fs" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestWatchDirOptions(t *testing.T) { + listCase := []struct { + expError string + opts DirWatcherOptions + }{{ + opts: DirWatcherOptions{ + Includes: []string{`.(*\.adoc$`}, + }, + expError: "WatchDir: error parsing regexp: missing argument to repetition operator: `*`", + }, { + opts: DirWatcherOptions{ + Excludes: []string{`.(*\.adoc$`}, + }, + expError: "WatchDir: error parsing regexp: missing argument to repetition operator: `*`", + }} + + for _, tc := range listCase { + _, err := WatchDir(tc.opts) + test.Assert(t, `error`, tc.expError, err.Error()) + } +} + +func TestWatchDirInitialScan(t *testing.T) { + listCase := []struct { + expFiles map[string]os.FileInfo + desc string + opts DirWatcherOptions + expIndex DirWatcher + }{{ + desc: `With includes and excludes`, + opts: DirWatcherOptions{ + FileWatcherOptions: FileWatcherOptions{ + File: `testdata/rescan`, + Interval: 50 * time.Millisecond, + }, + Root: `testdata/`, + Includes: []string{`.*\.adoc$`}, + Excludes: []string{`exc$`, `.*\.html$`, ` `}, + }, + expIndex: DirWatcher{ + idxDir: map[string]node{ + `testdata/`: node{ + name: `testdata/`, + size: 2, + mode: fs.ModeDir | 0755, + }, + `testdata/exc`: nodeExcluded, + `testdata/inc`: node{ + name: `testdata/inc`, + size: 3, + mode: fs.ModeDir | 0755, + }, + }, + idxFile: map[string]node{ + `testdata/inc/index.adoc`: node{ + name: `testdata/inc/index.adoc`, + size: 7, + mode: 0644, + }, + `testdata/inc/index.css`: nodeExcluded, + `testdata/inc/index.html`: nodeExcluded, + }, + }, + expFiles: map[string]os.FileInfo{ + `testdata/inc/index.adoc`: &node{ + name: `testdata/inc/index.adoc`, + size: 7, + mode: 0644, + }, + }, + }, { + desc: `With empty includes`, + opts: DirWatcherOptions{ + FileWatcherOptions: FileWatcherOptions{ + File: `testdata/rescan`, + Interval: 50 * time.Millisecond, + }, + Root: `testdata/`, + Includes: []string{` `}, + Excludes: []string{`exc$`, `.*\.adoc$`}, + }, + expIndex: DirWatcher{ + idxDir: map[string]node{ + `testdata/`: node{ + name: `testdata/`, + size: 2, + mode: fs.ModeDir | 0755, + }, + `testdata/exc`: nodeExcluded, + `testdata/inc`: node{ + name: `testdata/inc`, + size: 3, + mode: fs.ModeDir | 0755, + }, + }, + idxFile: map[string]node{ + `testdata/inc/index.adoc`: nodeExcluded, + `testdata/inc/index.css`: node{ + name: `testdata/inc/index.css`, + mode: 0644, + }, + `testdata/inc/index.html`: node{ + name: `testdata/inc/index.html`, + mode: 0644, + }, + }, + }, + expFiles: map[string]os.FileInfo{ + `testdata/inc/index.css`: &node{ + name: `testdata/inc/index.css`, + mode: 0644, + }, + `testdata/inc/index.html`: &node{ + name: `testdata/inc/index.html`, + mode: 0644, + }, + }, + }} + + for _, tc := range listCase { + dwatch, err := WatchDir(tc.opts) + if err != nil { + t.Fatal(err) + } + dwatch.Stop() + test.Assert(t, tc.desc+`: idxDir`, tc.expIndex.idxDir, + dwatch.idxDir) + test.Assert(t, tc.desc+`: idxFile`, tc.expIndex.idxFile, + dwatch.idxFile) + gotFiles := dwatch.Files() + test.Assert(t, tc.desc+`: Files`, tc.expFiles, gotFiles) + } +} + +func TestWatchDir(t *testing.T) { + var ( + dirTemp = t.TempDir() + fileToWatch = `rescan` + opts = DirWatcherOptions{ + FileWatcherOptions: FileWatcherOptions{ + File: filepath.Join(dirTemp, fileToWatch), + Interval: 50 * time.Millisecond, + }, + Root: dirTemp, + Includes: []string{`.*\.adoc$`}, + Excludes: []string{`exc$`, `.*\.html$`}, + } + dwatch *DirWatcher + err error + ) + + dwatch, err = WatchDir(opts) + if err != nil { + t.Fatal(err) + } + + var ( + fileAadoc = filepath.Join(opts.Root, `a.adoc`) + fileBadoc = filepath.Join(opts.Root, `b.adoc`) + ) + t.Run(`On included files created`, func(t *testing.T) { + var expNames = []string{ + fileAadoc, + fileBadoc, + } + for _, file := range expNames { + err = os.WriteFile(file, nil, 0600) + if err != nil { + t.Fatal(err) + } + } + + dwatch.ForceRescan() + + var gotNames = listFileName(<-dwatch.C) + test.Assert(t, `changes`, expNames, gotNames) + }) + + t.Run(`On dir excluded created`, func(t *testing.T) { + err = os.MkdirAll(filepath.Join(opts.Root, `exc`), 0700) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(opts.FileWatcherOptions.File, + []byte(`xx`), 0600) + if err != nil { + t.Fatal(err) + } + + var gotNames []string = listFileName(<-dwatch.C) + var expNames []string + test.Assert(t, `changes`, expNames, gotNames) + }) + + t.Run(`On file excluded created`, func(t *testing.T) { + var fileAhtml = filepath.Join(opts.Root, `a.html`) + err = os.WriteFile(fileAhtml, nil, 0600) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(opts.FileWatcherOptions.File, + []byte(`xxx`), 0600) + if err != nil { + t.Fatal(err) + } + + var gotNames []string = listFileName(<-dwatch.C) + var expNames []string + test.Assert(t, `changes`, expNames, gotNames) + }) + + t.Run(`On sub directory created`, func(t *testing.T) { + var dirInc = filepath.Join(opts.Root, `inc`) + err = os.MkdirAll(dirInc, 0700) + if err != nil { + t.Fatal(err) + } + var fileC = filepath.Join(dirInc, `ccc.adoc`) + err = os.WriteFile(fileC, nil, 0600) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(opts.FileWatcherOptions.File, + []byte(`xxxx`), 0600) + if err != nil { + t.Fatal(err) + } + + var gotNames []string = listFileName(<-dwatch.C) + var expNames = []string{ + fileC, + } + test.Assert(t, `changes`, expNames, gotNames) + }) + + t.Run(`On file updated`, func(t *testing.T) { + err = os.WriteFile(fileAadoc, nil, 0600) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(opts.FileWatcherOptions.File, + []byte(`xxxxx`), 0600) + if err != nil { + t.Fatal(err) + } + + var gotNames []string = listFileName(<-dwatch.C) + var expNames = []string{ + fileAadoc, + } + test.Assert(t, `changes`, expNames, gotNames) + }) + + t.Run(`On file deleted`, func(t *testing.T) { + err = os.Remove(fileAadoc) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(opts.FileWatcherOptions.File, + []byte(`xxxxx x`), 0600) + if err != nil { + t.Fatal(err) + } + + var gotNames []string = listFileName(<-dwatch.C) + var expNames = []string{ + fileAadoc, + } + test.Assert(t, `changes`, expNames, gotNames) + }) +} + +func listFileName(listfi []os.FileInfo) (listName []string) { + var fi os.FileInfo + for _, fi = range listfi { + listName = append(listName, fi.Name()) + } + sort.Strings(listName) + return listName +} diff --git a/lib/watchfs/v2/node.go b/lib/watchfs/v2/node.go new file mode 100644 index 00000000..27b51dba --- /dev/null +++ b/lib/watchfs/v2/node.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: BSD-3-Clause + +package watchfs + +import ( + "os" + "time" +) + +// FileFlagDeleted indicated that a file has been deleted. +// The flag is stored inside the [os.FileInfo.Size]. +const FileFlagDeleted = -1 + +const nodeFlagExcluded = -2 + +const nodeFlagForced = -3 + +var nodeExcluded = node{ + size: nodeFlagExcluded, +} + +type node struct { + // The file modification time. + // This field also store the flag for excluded and deleted file. + mtime time.Time `noequal:""` + + // The name contains the relative path, not only base name. + name string + + // Size of file. + // For directory the size contains the number of childs, the length of + // slice returned by [os.ReadDir]. + size int64 + + mode os.FileMode +} + +func newNode(apath string) (n *node, err error) { + var fi os.FileInfo + fi, err = os.Stat(apath) + if err != nil { + return nil, err + } + n = &node{ + name: apath, + mtime: fi.ModTime(), + mode: fi.Mode(), + } + if !fi.IsDir() { + n.size = fi.Size() + } + return n, nil +} + +func (node *node) IsDir() bool { + return node.mode.IsDir() +} + +func (node *node) Mode() os.FileMode { + return node.mode +} + +func (node *node) ModTime() time.Time { + return node.mtime +} + +// Name return the relative path to the file, not base name of file. +func (node *node) Name() string { + return node.name +} + +func (node *node) Size() int64 { + return node.size +} + +func (node *node) Sys() any { + return node +} + +func (node *node) equal(other *node) bool { + if !node.mtime.Equal(other.mtime) { + return false + } + if node.size != other.size { + return false + } + if node.mode != other.mode { + return false + } + return true +} diff --git a/lib/watchfs/v2/testdata/exc/index.adoc b/lib/watchfs/v2/testdata/exc/index.adoc new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/watchfs/v2/testdata/exc/index.adoc diff --git a/lib/watchfs/v2/testdata/inc/index.adoc b/lib/watchfs/v2/testdata/inc/index.adoc new file mode 100644 index 00000000..1d6006e8 --- /dev/null +++ b/lib/watchfs/v2/testdata/inc/index.adoc @@ -0,0 +1 @@ += test diff --git a/lib/watchfs/v2/testdata/inc/index.css b/lib/watchfs/v2/testdata/inc/index.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/watchfs/v2/testdata/inc/index.css diff --git a/lib/watchfs/v2/testdata/inc/index.html b/lib/watchfs/v2/testdata/inc/index.html new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/watchfs/v2/testdata/inc/index.html diff --git a/lib/watchfs/v2/watchfs.go b/lib/watchfs/v2/watchfs.go index f93c1914..0ee2fe78 100644 --- a/lib/watchfs/v2/watchfs.go +++ b/lib/watchfs/v2/watchfs.go @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> // SPDX-License-Identifier: BSD-3-Clause -// Package watchfs implement naive file watcher. +// Package watchfs implement naive file and directory watcher. package watchfs |
