feat(watch): add opt-in --backfill flag for existing archives

Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-07-03 15:44:08 +00:00
committed by GitHub
co-authored by Belphemur
parent 0b3c3fa90c
commit 1d981be21b
4 changed files with 59 additions and 14 deletions
+9 -6
View File
@@ -78,13 +78,15 @@ Watch a folder for new CBZ/CBR files and optimize them automatically:
cbzconverter watch [folder] --quality 85 --override --format webp --split
```
Watch mode only reacts to filesystem events that occur *after* it starts; it does not
scan and optimize files that already exist in the folder when it starts. Run the
Watch mode only reacts to filesystem events that occur *after* it starts; by default it does
not scan and optimize files that already exist in the folder when it starts. Run the
`optimize` command first if you need to process an existing library, then use `watch`
to keep it up to date going forward. The only exception is a directory that gets
created/moved into the watched tree while watch mode is running: since no per-file
event is emitted for files already inside it, its existing archives are processed once
when the directory is first detected.
to keep it up to date going forward, or pass `--backfill` to have `watch` optimize the
existing files at startup before it begins watching for new changes. The only exception
is a directory that gets created/moved into the watched tree while watch mode is
running: since no per-file event is emitted for files already inside it, its existing
archives are always processed once when the directory is first detected, regardless of
`--backfill`.
Or with Docker:
@@ -102,6 +104,7 @@ docker run -v /path/to/comics:/comics ghcr.io/belphemur/cbzoptimizer:latest watc
- Can be specified as: `--format webp`, `-f webp`, or `--format=webp`
- Case-insensitive: `webp`, `WEBP`, and `WebP` are all valid
- `--timeout`, `-t`: Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout. Default is 0.
- `--backfill`: *(`watch` only)* Optimize CBZ/CBR files that already exist in the watched folder at startup, before watching for new changes. Default is false.
- `--log`, `-l`: Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'. Default is info.
## Logging
+15 -7
View File
@@ -41,6 +41,9 @@ func init() {
// Setup common flags (format, quality, override, split, timeout) with viper binding
setupCommonFlags(command, &converterType, 85, true, false, true)
command.Flags().Bool("backfill", false, "Optimize CBZ/CBR files that already exist in the watched folder at startup, before watching for new changes")
_ = viper.BindPFlag("backfill", command.Flags().Lookup("backfill"))
AddCommand(command)
}
func WatchCommand(_ *cobra.Command, args []string) error {
@@ -64,6 +67,8 @@ func WatchCommand(_ *cobra.Command, args []string) error {
timeout := viper.GetDuration("timeout")
backfill := viper.GetBool("backfill")
converterType := constant.FindConversionFormat(viper.GetString("format"))
chapterConverter, err := converter.Get(converterType)
if err != nil {
@@ -74,7 +79,7 @@ func WatchCommand(_ *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("failed to prepare converter: %w", err)
}
log.Info().Str("path", path).Bool("override", override).Uint8("quality", quality).Str("format", converterType.String()).Bool("split", split).Msg("Watching directory")
log.Info().Str("path", path).Bool("override", override).Uint8("quality", quality).Str("format", converterType.String()).Bool("split", split).Bool("backfill", backfill).Msg("Watching directory")
watcher, err := fsnotify.NewWatcher()
if err != nil {
@@ -101,15 +106,18 @@ func WatchCommand(_ *cobra.Command, args []string) error {
defer debouncer.Stop()
// Note: existing archives already present under path when the watch
// starts are intentionally left untouched. Watch mode only reacts to
// filesystem events going forward; use the `optimize` command to process
// a library's existing contents. Archives inside a directory that is
// created/moved into the watched tree *after* startup are still
// back-filled below, since only the directory itself generates an
// fsnotify event.
// starts are left untouched unless --backfill is set. Watch mode only
// reacts to filesystem events going forward by default; use the
// `optimize` command (or pass --backfill) to process a library's
// existing contents. Archives inside a directory that is created/moved
// into the watched tree *after* startup are always back-filled below,
// since only the directory itself generates an fsnotify event.
if err := addRecursiveWatch(watcher, path); err != nil {
return fmt.Errorf("failed to watch path %s: %w", path, err)
}
if backfill {
backfillExistingArchives(path, debouncer.Trigger)
}
for {
select {
@@ -170,3 +170,37 @@ func TestOptimizeQueueSkipsMissingPath(t *testing.T) {
assert.EqualValues(t, 1, atomic.LoadInt32(&calls))
}
func TestWatchCommandBackfillFlagDefaultsToFalse(t *testing.T) {
watchCmd, _, err := rootCmd.Find([]string{"watch"})
require.NoError(t, err)
flag := watchCmd.Flags().Lookup("backfill")
require.NotNil(t, flag, "watch command should register a --backfill flag")
assert.Equal(t, "false", flag.DefValue)
assert.Equal(t, "bool", flag.Value.Type())
}
func TestBackfillExistingArchivesOnlyInvokedWhenRequested(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "chapter1.cbz"), []byte("data"), 0o644))
runBackfill := func(backfill bool) []string {
var found []string
var mu sync.Mutex
process := func(path string) {
mu.Lock()
defer mu.Unlock()
found = append(found, path)
}
// Mirrors the gating logic in WatchCommand: backfillExistingArchives
// must only run when the --backfill flag is set.
if backfill {
backfillExistingArchives(root, process)
}
return found
}
assert.Empty(t, runBackfill(false), "no pre-existing archive should be processed when backfill is disabled")
assert.Len(t, runBackfill(true), 1, "pre-existing archives should be processed when backfill is enabled")
}
+1 -1
View File
@@ -19,7 +19,7 @@ CBZOptimizer is a Go CLI that optimizes comic archives (`.cbz` and `.cbr`) by co
## Watch mode
`watch` monitors a directory tree for archive file changes and runs optimization automatically.
`watch` monitors a directory tree for archive file changes and runs optimization automatically. By default it only reacts to changes going forward; pass `--backfill` to also optimize archives that already exist in the folder at startup.
## Key runtime requirements