From 1d981be21ba7c6189cc26d1e82a2355879fe7204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:44:08 +0000 Subject: [PATCH] feat(watch): add opt-in --backfill flag for existing archives Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com> --- README.md | 15 ++++---- cmd/cbzoptimizer/commands/watch_command.go | 22 ++++++++---- .../commands/watch_command_test.go | 34 +++++++++++++++++++ docs/project-overview.md | 2 +- 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a16ed13..6ccdb36 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/cbzoptimizer/commands/watch_command.go b/cmd/cbzoptimizer/commands/watch_command.go index b4b7684..e7447dc 100644 --- a/cmd/cbzoptimizer/commands/watch_command.go +++ b/cmd/cbzoptimizer/commands/watch_command.go @@ -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 { diff --git a/cmd/cbzoptimizer/commands/watch_command_test.go b/cmd/cbzoptimizer/commands/watch_command_test.go index 485c3fd..d3a4222 100644 --- a/cmd/cbzoptimizer/commands/watch_command_test.go +++ b/cmd/cbzoptimizer/commands/watch_command_test.go @@ -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") +} diff --git a/docs/project-overview.md b/docs/project-overview.md index 0a566c2..1531dfe 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -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