From 4a2d50ed002f2e2ca2184419d259f4396f45fbc0 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Thu, 20 Aug 2026 11:11:05 +0100 Subject: [PATCH] cover the whole API in the example and let it observe an event the example set both watchers up, printed a line and returned, so the deferred Close calls fired before anything could be reported and a reader copying it got a programme that never watches. it now makes a change and waits for the event, with a bounded timeout so a slow filesystem cannot hang it, and reports what Close returns instead of discarding it. MoveFile, IsFile, IsDir, TouchFile, TempFileName and the MD5 algorithm lost their only usage when the README snippets were replaced. they are back, in the one programme that is compiled and run by the tests. --- README.md | 80 +++++++++++++++++++++++++++++++++++------- examples/basic/main.go | 76 +++++++++++++++++++++++++++++++++------ 2 files changed, 132 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 201e22d..24e9782 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Package `fileutils` provides useful, high-level file operations. ## Complete example -The following program creates and copies a file, calculates its SHA-256 checksum, and configures both watcher variants. It is also tracked as [`examples/basic/main.go`](examples/basic/main.go). +The following program copies and moves a file, checks what exists, calculates MD5 and SHA-256 checksums, reserves a temporary name, and then runs both watcher variants until a change is reported. It is also tracked as [`examples/basic/main.go`](examples/basic/main.go). ```go @@ -29,6 +29,7 @@ import ( "log" "os" "path/filepath" + "time" "github.com/go-pkgz/fileutils" "github.com/go-pkgz/fileutils/enum" @@ -45,38 +46,77 @@ func run() error { if err != nil { return err } - defer os.RemoveAll(workDir) + defer func() { _ = os.RemoveAll(workDir) }() source := filepath.Join(workDir, "source.txt") if writeErr := os.WriteFile(source, []byte("fileutils example\n"), 0o600); writeErr != nil { return writeErr } - destination := filepath.Join(workDir, "copied.txt") - if copyErr := fileutils.CopyFile(source, destination); copyErr != nil { + // copy the file, then move the copy into a directory that does not exist yet + copied := filepath.Join(workDir, "copied.txt") + if copyErr := fileutils.CopyFile(source, copied); copyErr != nil { return copyErr } + moved := filepath.Join(workDir, "archive", "moved.txt") + if moveErr := fileutils.MoveFile(copied, moved); moveErr != nil { + return moveErr + } + fmt.Printf("IsFile(moved.txt): %v\n", fileutils.IsFile(moved)) + fmt.Printf("IsDir(archive): %v\n", fileutils.IsDir(filepath.Dir(moved))) + + // any algorithm from the enum package works the same way + md5sum, err := fileutils.Checksum(moved, enum.HashAlgMD5) + if err != nil { + return err + } + sha256sum, err := fileutils.Checksum(moved, enum.HashAlgSHA256) + if err != nil { + return err + } + fmt.Printf("MD5: %s\n", md5sum) + fmt.Printf("SHA-256: %s\n", sha256sum) - checksum, err := fileutils.Checksum(destination, enum.HashAlgSHA256) + // a free name to write to, the file itself is not created + tempName, err := fileutils.TempFileName(workDir, "upload-*.tmp") if err != nil { return err } - fmt.Printf("SHA-256: %s\n", checksum) + fmt.Printf("temp name: %s\n", filepath.Base(tempName)) + return watch(workDir, source) +} + +// watch starts both watcher variants, makes a change and waits for it to be reported. +func watch(workDir, source string) error { + // the callback runs on the watcher goroutine, so hand the event over instead of working here + events := make(chan fileutils.FileEvent, 16) handleEvent := func(event fileutils.FileEvent) { - fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + select { + case events <- event: + default: // the buffer is enough for this example, drop the rest + } } fileWatcher, err := fileutils.NewFileWatcher(source, handleEvent) if err != nil { return err } - defer fileWatcher.Close() - - if addErr := fileWatcher.AddPath(destination); addErr != nil { + defer func() { + if closeErr := fileWatcher.Close(); closeErr != nil { + log.Printf("close file watcher: %v", closeErr) + } + }() + + // paths can be added and removed while the watcher runs + extra := filepath.Join(workDir, "extra.txt") + if touchErr := fileutils.TouchFile(extra); touchErr != nil { + return touchErr + } + if addErr := fileWatcher.AddPath(extra); addErr != nil { return addErr } - if removeErr := fileWatcher.RemovePath(destination); removeErr != nil { + if removeErr := fileWatcher.RemovePath(extra); removeErr != nil { return removeErr } @@ -84,9 +124,23 @@ func run() error { if err != nil { return err } - defer recursiveWatcher.Close() + defer func() { + if closeErr := recursiveWatcher.Close(); closeErr != nil { + log.Printf("close recursive watcher: %v", closeErr) + } + }() + + // the paths are registered before the constructors return, so this change is already watched + if changeErr := os.WriteFile(source, []byte("changed\n"), 0o600); changeErr != nil { + return changeErr + } - fmt.Printf("Watching %s\n", workDir) + select { + case event := <-events: + fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + case <-time.After(5 * time.Second): + fmt.Println("no event within the timeout") + } return nil } ``` diff --git a/examples/basic/main.go b/examples/basic/main.go index 6f8f163..ad00f91 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "time" "github.com/go-pkgz/fileutils" "github.com/go-pkgz/fileutils/enum" @@ -21,38 +22,77 @@ func run() error { if err != nil { return err } - defer os.RemoveAll(workDir) + defer func() { _ = os.RemoveAll(workDir) }() source := filepath.Join(workDir, "source.txt") if writeErr := os.WriteFile(source, []byte("fileutils example\n"), 0o600); writeErr != nil { return writeErr } - destination := filepath.Join(workDir, "copied.txt") - if copyErr := fileutils.CopyFile(source, destination); copyErr != nil { + // copy the file, then move the copy into a directory that does not exist yet + copied := filepath.Join(workDir, "copied.txt") + if copyErr := fileutils.CopyFile(source, copied); copyErr != nil { return copyErr } + moved := filepath.Join(workDir, "archive", "moved.txt") + if moveErr := fileutils.MoveFile(copied, moved); moveErr != nil { + return moveErr + } + fmt.Printf("IsFile(moved.txt): %v\n", fileutils.IsFile(moved)) + fmt.Printf("IsDir(archive): %v\n", fileutils.IsDir(filepath.Dir(moved))) + + // any algorithm from the enum package works the same way + md5sum, err := fileutils.Checksum(moved, enum.HashAlgMD5) + if err != nil { + return err + } + sha256sum, err := fileutils.Checksum(moved, enum.HashAlgSHA256) + if err != nil { + return err + } + fmt.Printf("MD5: %s\n", md5sum) + fmt.Printf("SHA-256: %s\n", sha256sum) - checksum, err := fileutils.Checksum(destination, enum.HashAlgSHA256) + // a free name to write to, the file itself is not created + tempName, err := fileutils.TempFileName(workDir, "upload-*.tmp") if err != nil { return err } - fmt.Printf("SHA-256: %s\n", checksum) + fmt.Printf("temp name: %s\n", filepath.Base(tempName)) + return watch(workDir, source) +} + +// watch starts both watcher variants, makes a change and waits for it to be reported. +func watch(workDir, source string) error { + // the callback runs on the watcher goroutine, so hand the event over instead of working here + events := make(chan fileutils.FileEvent, 16) handleEvent := func(event fileutils.FileEvent) { - fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + select { + case events <- event: + default: // the buffer is enough for this example, drop the rest + } } fileWatcher, err := fileutils.NewFileWatcher(source, handleEvent) if err != nil { return err } - defer fileWatcher.Close() + defer func() { + if closeErr := fileWatcher.Close(); closeErr != nil { + log.Printf("close file watcher: %v", closeErr) + } + }() - if addErr := fileWatcher.AddPath(destination); addErr != nil { + // paths can be added and removed while the watcher runs + extra := filepath.Join(workDir, "extra.txt") + if touchErr := fileutils.TouchFile(extra); touchErr != nil { + return touchErr + } + if addErr := fileWatcher.AddPath(extra); addErr != nil { return addErr } - if removeErr := fileWatcher.RemovePath(destination); removeErr != nil { + if removeErr := fileWatcher.RemovePath(extra); removeErr != nil { return removeErr } @@ -60,8 +100,22 @@ func run() error { if err != nil { return err } - defer recursiveWatcher.Close() + defer func() { + if closeErr := recursiveWatcher.Close(); closeErr != nil { + log.Printf("close recursive watcher: %v", closeErr) + } + }() - fmt.Printf("Watching %s\n", workDir) + // the paths are registered before the constructors return, so this change is already watched + if changeErr := os.WriteFile(source, []byte("changed\n"), 0o600); changeErr != nil { + return changeErr + } + + select { + case event := <-events: + fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + case <-time.After(5 * time.Second): + fmt.Println("no event within the timeout") + } return nil }