summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-12-15 00:22:14 +0700
committerShulhan <ms@kilabit.info>2024-12-28 16:26:30 +0700
commit5f97e1d495dd341f2bf573d80b0edb4d02a78a7b (patch)
tree6260d4f733c29e83772744ee28dd833502556da4
parentf8ca0454d4c85f477c99304b9bf362dc3b5ab3d3 (diff)
downloadpakakeh.go-5f97e1d495dd341f2bf573d80b0edb4d02a78a7b.tar.xz
watchfs/v2: implement new file watcher
The watchfs package contains new FileWatcher, more simple than what we have in [memfs.Watcher].
-rw-r--r--README.md5
-rw-r--r--lib/watchfs/v2/file_watcher.go85
-rw-r--r--lib/watchfs/v2/file_watcher_example_test.go59
-rw-r--r--lib/watchfs/v2/file_watcher_options.go15
-rw-r--r--lib/watchfs/v2/file_watcher_test.go182
-rw-r--r--lib/watchfs/v2/watchfs.go5
6 files changed, 351 insertions, 0 deletions
diff --git a/README.md b/README.md
index 713dd8c1..b9adad4f 100644
--- a/README.md
+++ b/README.md
@@ -244,6 +244,11 @@ A library for working with time.
Package totp implement Time-Based One-Time Password Algorithm based on RFC
6238.
+[**watchfs/v2**](https://pkg.go.dev/git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2)::
+Package watchfs implement naive file watcher.
+The version 2 simplify watching single file and directory.
+For directory watcher, it watch only one file instead of all included files.
+
[**websocket**](https://pkg.go.dev/git.sr.ht/~shulhan/pakakeh.go/lib/websocket)::
The WebSocket library for server and client. This WebSocket library has
been tested with autobahn testsuite with 100% success rate.
diff --git a/lib/watchfs/v2/file_watcher.go b/lib/watchfs/v2/file_watcher.go
new file mode 100644
index 00000000..c66a7882
--- /dev/null
+++ b/lib/watchfs/v2/file_watcher.go
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: BSD-3-Clause
+
+package watchfs
+
+import (
+ "errors"
+ "os"
+ "time"
+)
+
+// FileWatcher watch a single file.
+// It will send the [os.FileInfo] to the channel C when the file is created,
+// updated; or nil if file has been deleted.
+//
+// The FileWatcher may stop unexpectedly when the [os.Stat] return an
+// error other than [os.ErrNotExist].
+// The last error can be inspected using [FileWatcher.Err].
+type FileWatcher struct {
+ fstat os.FileInfo
+ err error
+
+ // C receive new file information.
+ C <-chan os.FileInfo
+ c chan os.FileInfo
+
+ ticker *time.Ticker
+ opts FileWatcherOptions
+}
+
+// WatchFile watch the file [watchfs.FileWatcherOptions.File] for being
+// created, updated, or deleted; on every
+// [watchfs.FileWatcherOptions.Interval].
+func WatchFile(opts FileWatcherOptions) (fwatch *FileWatcher) {
+ fwatch = &FileWatcher{
+ c: make(chan os.FileInfo, 1),
+ ticker: time.NewTicker(opts.Interval),
+ opts: opts,
+ }
+ fwatch.fstat, fwatch.err = os.Stat(opts.File)
+ fwatch.C = fwatch.c
+ go fwatch.watch()
+ return fwatch
+}
+
+// Err return the last error that cause the watch stopped.
+func (fwatch *FileWatcher) Err() error {
+ return fwatch.err
+}
+
+// Stop watching the file.
+func (fwatch *FileWatcher) Stop() {
+ fwatch.ticker.Stop()
+ close(fwatch.c)
+}
+
+func (fwatch *FileWatcher) watch() {
+ var newStat os.FileInfo
+ for range fwatch.ticker.C {
+ newStat, fwatch.err = os.Stat(fwatch.opts.File)
+ if fwatch.err != nil {
+ if errors.Is(fwatch.err, os.ErrNotExist) {
+ if fwatch.fstat != nil {
+ // File deleted.
+ fwatch.fstat = nil
+ fwatch.c <- nil
+ }
+ continue
+ }
+ // Other errors cause the watcher stop unexpectedly.
+ fwatch.Stop()
+ return
+ }
+ if fwatch.fstat != nil {
+ if newStat.Mode() == fwatch.fstat.Mode() &&
+ newStat.ModTime().Equal(fwatch.fstat.ModTime()) &&
+ newStat.Size() == fwatch.fstat.Size() {
+ continue
+ }
+ }
+ // File created or updated.
+ fwatch.fstat = newStat
+ fwatch.c <- newStat
+ }
+}
diff --git a/lib/watchfs/v2/file_watcher_example_test.go b/lib/watchfs/v2/file_watcher_example_test.go
new file mode 100644
index 00000000..06672eb9
--- /dev/null
+++ b/lib/watchfs/v2/file_watcher_example_test.go
@@ -0,0 +1,59 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: BSD-3-Clause
+
+package watchfs_test
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2"
+)
+
+func ExampleWatchFile() {
+ var (
+ name = `file.txt`
+ opts = watchfs.FileWatcherOptions{
+ File: filepath.Join(os.TempDir(), name),
+ Interval: 50 * time.Millisecond,
+ }
+ )
+
+ fwatch := watchfs.WatchFile(opts)
+
+ // On create ...
+ _, err := os.Create(opts.File)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var fi os.FileInfo = <-fwatch.C
+ fmt.Printf("file %q created\n", fi.Name())
+
+ // On update ...
+ err = os.WriteFile(opts.File, nil, 0600)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fi = <-fwatch.C
+ fmt.Printf("file %q updated\n", fi.Name())
+
+ // On delete ...
+ err = os.Remove(opts.File)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fi = <-fwatch.C
+ fmt.Printf("file deleted: %v\n", fi)
+
+ fwatch.Stop()
+
+ // Output:
+ // file "file.txt" created
+ // file "file.txt" updated
+ // file deleted: <nil>
+}
diff --git a/lib/watchfs/v2/file_watcher_options.go b/lib/watchfs/v2/file_watcher_options.go
new file mode 100644
index 00000000..ca9c0834
--- /dev/null
+++ b/lib/watchfs/v2/file_watcher_options.go
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: BSD-3-Clause
+
+package watchfs
+
+import "time"
+
+// FileWatcherOptions define the options to watch file.
+type FileWatcherOptions struct {
+ // Path to the file to be watched.
+ File string
+
+ // Interval to check for file changes.
+ Interval time.Duration
+}
diff --git a/lib/watchfs/v2/file_watcher_test.go b/lib/watchfs/v2/file_watcher_test.go
new file mode 100644
index 00000000..7c4fa7fe
--- /dev/null
+++ b/lib/watchfs/v2/file_watcher_test.go
@@ -0,0 +1,182 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: BSD-3-Clause
+
+package watchfs_test
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/reflect"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2"
+)
+
+type directFileInfo struct {
+ modTime time.Time
+ reflect.Equaler
+ name string
+ size int64
+ mode os.FileMode
+ isDir bool
+}
+
+func newDirectFileInfo(fi os.FileInfo) (directfi *directFileInfo) {
+ if fi == nil {
+ return nil
+ }
+ directfi = &directFileInfo{
+ name: fi.Name(),
+ size: fi.Size(),
+ mode: fi.Mode(),
+ modTime: fi.ModTime().Truncate(time.Second),
+ isDir: fi.IsDir(),
+ }
+ return directfi
+}
+
+func (directfi directFileInfo) Equal(v any) (err error) {
+ var (
+ other *directFileInfo
+ ok bool
+ )
+ other, ok = v.(*directFileInfo)
+ if !ok {
+ return fmt.Errorf(`expecting type %T, got %T`, other, v)
+ }
+
+ if directfi.name != other.name {
+ return fmt.Errorf(`name: got %s, want %s`,
+ other.name, directfi.name)
+ }
+ if directfi.size != other.size {
+ return fmt.Errorf(`size: got %d, want %d`,
+ other.size, directfi.size)
+ }
+ if directfi.mode != other.mode {
+ return fmt.Errorf(`filemode: got %d, want %d`,
+ other.mode, directfi.mode)
+ }
+ if directfi.modTime.IsZero() {
+ directfi.modTime = other.modTime
+ } else if directfi.modTime.After(other.modTime) {
+ return fmt.Errorf(`modTime: got %v, want %v`,
+ other.modTime, directfi.modTime)
+ }
+ if directfi.isDir != other.isDir {
+ return fmt.Errorf(`isDir: got %t, want %t`,
+ other.isDir, directfi.isDir)
+ }
+ return nil
+}
+
+func TestWatchFile(t *testing.T) {
+ var (
+ name = `file.txt`
+ opts = watchfs.FileWatcherOptions{
+ File: filepath.Join(t.TempDir(), name),
+ Interval: 50 * time.Millisecond,
+ }
+
+ fwatch = watchfs.WatchFile(opts)
+ expfi = &directFileInfo{
+ name: name,
+ size: 0,
+ mode: 420,
+ }
+
+ file *os.File
+ err error
+ )
+
+ t.Run(`On created`, func(t *testing.T) {
+ file, err = os.Create(opts.File)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // It should trigger an event.
+ var fi os.FileInfo = <-fwatch.C
+ var gotfi = newDirectFileInfo(fi)
+ test.Assert(t, `fwatch.C`, expfi, gotfi)
+
+ fi, err = file.Stat()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var orgfi = newDirectFileInfo(fi)
+ test.Assert(t, `created `+opts.File+` on FileInfo`,
+ orgfi, gotfi)
+
+ expfi.modTime = orgfi.modTime
+ })
+
+ t.Run(`On update content`, func(t *testing.T) {
+ var expBody = `update`
+ _, err = file.WriteString(expBody)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = file.Sync()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var fi os.FileInfo = <-fwatch.C
+ var gotfi = newDirectFileInfo(fi)
+ expfi.size = 6
+ test.Assert(t, `fwatch.C`, expfi, gotfi)
+
+ var gotBody []byte
+ gotBody, err = os.ReadFile(opts.File)
+ if err != nil {
+ t.Fatal(err)
+ }
+ test.Assert(t, `body`, expBody, string(gotBody))
+
+ expfi.modTime = gotfi.modTime
+ })
+
+ t.Run(`On update mode`, func(t *testing.T) {
+ var expMode = os.FileMode(0750)
+ err = file.Chmod(expMode)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var fi os.FileInfo = <-fwatch.C
+ var gotfi = newDirectFileInfo(fi)
+ expfi.mode = expMode
+ test.Assert(t, `fwatch.C`, expfi, gotfi)
+
+ expfi.modTime = gotfi.modTime
+ })
+
+ t.Run(`On deleted`, func(t *testing.T) {
+ err = file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Remove(opts.File)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var fi os.FileInfo = <-fwatch.C
+ var gotfi = newDirectFileInfo(fi)
+ var nilfi *directFileInfo
+ test.Assert(t, `fwatch.C`, nilfi, gotfi)
+
+ fwatch.Stop()
+
+ var expError = `no such file or directory`
+ var gotError = fwatch.Err().Error()
+ if !strings.Contains(gotError, expError) {
+ t.Fatalf(`error does not contains %q`, expError)
+ }
+ })
+}
diff --git a/lib/watchfs/v2/watchfs.go b/lib/watchfs/v2/watchfs.go
new file mode 100644
index 00000000..f93c1914
--- /dev/null
+++ b/lib/watchfs/v2/watchfs.go
@@ -0,0 +1,5 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package watchfs implement naive file watcher.
+package watchfs