Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 92 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<!-- fileutils-example-start -->
```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)
```
<!-- fileutils-example-end -->

### 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
```
67 changes: 67 additions & 0 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions readme_test.go
Original file line number Diff line number Diff line change
@@ -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 = "<!-- fileutils-example-start -->\n```go\n"
const endMarker = "\n```\n<!-- fileutils-example-end -->"
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:")
}