diff --git a/README.md b/README.md index d1c4064..201e22d 100644 --- a/README.md +++ b/README.md @@ -4,97 +4,115 @@ Package `fileutils` provides useful, high-level file operations. ## Details -- `IsFile` & `IsDir` checks if file/directory exists -- `CopyFile` copies a file from source to destination, preserving mode, and refuses to copy a file onto itself -- `CopyDir` copies all files recursively from the source to destination directory -- `MoveFile` moves a file, using atomic rename when possible with copy+delete fallback -- `ListFiles` returns sorted slice of file paths in directory +- `IsFile` and `IsDir` check whether a file or directory exists +- `CopyFile` copies a file from source to destination, preserving its mode, and refuses to copy a file onto itself +- `CopyDir` copies all files recursively from the source to the destination directory +- `MoveFile` moves a file, using atomic rename when possible with a copy-and-delete fallback +- `ListFiles` returns a sorted slice of file paths in a directory - `TempFileName` returns a new temporary file name using secure random generation -- `SanitizePath` cleans file path -- `TouchFile` creates an empty file or updates timestamps of existing one -- `Checksum` calculates file checksum using various hash algorithms (MD5, SHA1, SHA256, etc.) +- `SanitizePath` cleans a file path +- `TouchFile` creates an empty file or updates the timestamps of an existing one +- `Checksum` calculates a file checksum using MD5, SHA-1, SHA-2 and related algorithms - `FileWatcher` watches files or directories for changes - `WatchRecursive` watches a directory recursively for changes -## Usage Examples +## Complete example -### File Operations +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). + ```go -// Copy a file -err := fileutils.CopyFile("source.txt", "destination.txt") -if err != nil { - log.Fatalf("Failed to copy file: %v", err) +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/go-pkgz/fileutils" + "github.com/go-pkgz/fileutils/enum" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } } -// Move a file -err = fileutils.MoveFile("source.txt", "destination.txt") -if err != nil { - log.Fatalf("Failed to move file: %v", err) +func run() error { + workDir, err := os.MkdirTemp("", "fileutils-example-*") + if err != nil { + return err + } + defer 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 { + return copyErr + } + + checksum, err := fileutils.Checksum(destination, enum.HashAlgSHA256) + if err != nil { + return err + } + fmt.Printf("SHA-256: %s\n", checksum) + + handleEvent := func(event fileutils.FileEvent) { + fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + } + + fileWatcher, err := fileutils.NewFileWatcher(source, handleEvent) + if err != nil { + return err + } + defer fileWatcher.Close() + + if addErr := fileWatcher.AddPath(destination); addErr != nil { + return addErr + } + if removeErr := fileWatcher.RemovePath(destination); removeErr != nil { + return removeErr + } + + recursiveWatcher, err := fileutils.WatchRecursive(workDir, handleEvent) + if err != nil { + return err + } + defer recursiveWatcher.Close() + + fmt.Printf("Watching %s\n", workDir) + return nil } - -// Check if a file or directory exists -if fileutils.IsFile("file.txt") { - fmt.Println("File exists") -} -if fileutils.IsDir("directory") { - fmt.Println("Directory exists") -} - -// Generate a temporary file name -tempName, err := fileutils.TempFileName("/tmp", "prefix-*.ext") -if err != nil { - log.Fatalf("Failed to generate temp file name: %v", err) -} -fmt.Println("Temp file:", tempName) ``` + -### File Checksum - -```go -// Calculate MD5 checksum -md5sum, err := fileutils.Checksum("path/to/file", enum.HashAlgMD5) -if err != nil { - log.Fatalf("Failed to calculate MD5: %v", err) -} -fmt.Printf("MD5: %s\n", md5sum) +To run this exact program from a repository checkout: -// Calculate SHA256 checksum -sha256sum, err := fileutils.Checksum("path/to/file", enum.HashAlgSHA256) -if err != nil { - log.Fatalf("Failed to calculate SHA256: %v", err) -} -fmt.Printf("SHA256: %s\n", sha256sum) +```sh +git clone https://github.com/go-pkgz/fileutils.git +cd fileutils +go mod download +go run ./examples/basic ``` -### File Watcher +To copy it into a new module instead, save the program as `main.go` and run: -```go -// Create a simple file watcher -watcher, err := fileutils.NewFileWatcher("/path/to/file", func(event FileEvent) { - fmt.Printf("Event: %s, Path: %s\n", event.Type, event.Path) -}) -if err != nil { - log.Fatalf("Failed to create watcher: %v", err) -} -defer watcher.Close() - -// Watch a directory recursively -watcher, err := fileutils.WatchRecursive("/path/to/dir", func(event FileEvent) { - fmt.Printf("Event: %s, Path: %s\n", event.Type, event.Path) -}) -if err != nil { - log.Fatalf("Failed to create watcher: %v", err) -} -defer watcher.Close() - -// Add another path to an existing watcher -err = watcher.AddPath("/path/to/another/file") - -// Remove a path from the watcher -err = watcher.RemovePath("/path/to/file") +```sh +mkdir fileutils-example +cd fileutils-example +go mod init example.com/fileutils-example +go get github.com/go-pkgz/fileutils@latest +go run . ``` ## Install and update -`go get -u github.com/go-pkgz/fileutils` +```sh +go get github.com/go-pkgz/fileutils@latest +``` diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..6f8f163 --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/go-pkgz/fileutils" + "github.com/go-pkgz/fileutils/enum" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + workDir, err := os.MkdirTemp("", "fileutils-example-*") + if err != nil { + return err + } + defer 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 { + return copyErr + } + + checksum, err := fileutils.Checksum(destination, enum.HashAlgSHA256) + if err != nil { + return err + } + fmt.Printf("SHA-256: %s\n", checksum) + + handleEvent := func(event fileutils.FileEvent) { + fmt.Printf("Event: %s, path: %s\n", event.Type, event.Path) + } + + fileWatcher, err := fileutils.NewFileWatcher(source, handleEvent) + if err != nil { + return err + } + defer fileWatcher.Close() + + if addErr := fileWatcher.AddPath(destination); addErr != nil { + return addErr + } + if removeErr := fileWatcher.RemovePath(destination); removeErr != nil { + return removeErr + } + + recursiveWatcher, err := fileutils.WatchRecursive(workDir, handleEvent) + if err != nil { + return err + } + defer recursiveWatcher.Close() + + fmt.Printf("Watching %s\n", workDir) + return nil +} diff --git a/readme_test.go b/readme_test.go new file mode 100644 index 0000000..9d9022d --- /dev/null +++ b/readme_test.go @@ -0,0 +1,32 @@ +package fileutils + +import ( + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadmeExample(t *testing.T) { + readme, err := os.ReadFile("README.md") //nolint:gosec // repository documentation is the test input + require.NoError(t, err) + + const startMarker = "\n```go\n" + const endMarker = "\n```\n" + start := strings.Index(string(readme), startMarker) + require.NotEqual(t, -1, start, "README example start marker is missing") + start += len(startMarker) + end := strings.Index(string(readme)[start:], endMarker) + require.NotEqual(t, -1, end, "README example end marker is missing") + + example, err := os.ReadFile("examples/basic/main.go") //nolint:gosec // repository example is the test input + require.NoError(t, err) + require.Equal(t, strings.TrimSpace(string(example)), strings.TrimSpace(string(readme)[start:start+end])) + + cmd := exec.Command("go", "run", "./examples/basic") //nolint:gosec // fixed repository example path + output, err := cmd.CombinedOutput() + require.NoErrorf(t, err, "documented example failed: %s", output) + require.Contains(t, string(output), "SHA-256:") +}