s3watcher
pkg/s3watcher is a standalone package for watching S3 objects. It polls via HEAD requests, keeps file contents in memory, and fires callbacks when objects change.
s3site uses s3watcher internally, but it works fine on its own if you just need to watch a few S3 objects.
Install
go get github.com/rhnvrm/s3site/pkg/s3watcher@latest
Basic usage
import "github.com/rhnvrm/s3site/pkg/s3watcher"
w, err := s3watcher.New(s3watcher.Config{
S3: s3Client,
PollInterval: 30 * time.Second,
Files: []s3watcher.FileEntry{
{Name: "config", Bucket: "my-bucket", Key: "app/config.json"},
{Name: "rules", Bucket: "my-bucket", Key: "app/rules.yaml"},
},
})
// Register a callback for changes.
w.OnUpdate(func(e s3watcher.UpdateEvent) {
log.Printf("%s changed (etag: %s -> %s)", e.Name, e.OldETag, e.NewETag)
})
// Start polling in the background.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go w.Start(ctx)
// Read a file. Returns content and current ETag.
data, etag := w.Get("config")
Lazy loading
Files are loaded lazily by default. The poll loop only does HEAD requests to check ETags. Content is fetched on the first Get() call or fs.FS read. If you're watching 50 files but only reading 3, only those 3 get downloaded.
HEAD-first polling
Each poll cycle:
- HEAD request to check the ETag
- If ETag matches the cached version, skip
- If ETag changed, GET the new content
- Fire the update callback
This minimizes bandwidth -- most polls are just HEAD requests.
fs.FS interface
s3watcher exposes watched files as an fs.FS:
fsys := w.FS()
// Use with http.FileServerFS, embed, template.ParseFS, etc.
data, err := fs.ReadFile(fsys, "config")
File names in the FS are the Name fields from your FileEntry list.
Cache management
// Evict a file from memory (re-fetched on next access).
w.Evict("config")
// Get stats.
stats := w.Stats()
fmt.Printf("files: %d, cached: %d, bytes: %d\n",
stats.Files, stats.Cached, stats.Bytes)
Config
type Config struct {
S3 *simples3.S3
PollInterval time.Duration // default: 1 minute
Files []FileEntry
Logger *slog.Logger // optional
}
type FileEntry struct {
Name string // lookup key (used in Get, FS, callbacks)
Bucket string
Key string
}