Compare commits

...
8 Commits
Author SHA1 Message Date
Antoine AflaloandGitHub e87e73eb28 Merge pull request #210 from Belphemur/copilot/optimize-file-processing
fix(watch): don't backfill existing archives at watch startup
2026-07-03 11:53:11 -04:00
d7782fdb25 refactor(watch): extract testable maybeBackfillExistingArchives helper
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2026-07-03 15:45:35 +00:00
1d981be21b feat(watch): add opt-in --backfill flag for existing archives
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2026-07-03 15:44:08 +00:00
0b3c3fa90c fix(watch): don't backfill existing archives at watch startup
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2026-07-03 15:38:44 +00:00
Antoine AflaloandGitHub 52c93acd35 Merge pull request #209 from Belphemur/copilot/fix-docker-permission-denied-issue
fix(docker): create and chown /config home directory for non-root user
2026-07-03 11:17:27 -04:00
ab1e2ff05c fix(docker): create and chown /config home directory for non-root user
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2026-07-03 15:15:49 +00:00
Antoine AflaloandGitHub eca9671881 Merge pull request #208 from Belphemur/copilot/move-to-alpine-latest-docker-image
chore(docker): switch base image to alpine:latest
2026-07-03 10:57:52 -04:00
40922078d5 chore(docker): switch base image to alpine:latest
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2026-07-03 14:53:55 +00:00
5 changed files with 87 additions and 24 deletions
+18 -21
View File
@@ -1,37 +1,34 @@
FROM debian:trixie-slim FROM alpine:latest
LABEL authors="Belphemur" LABEL authors="Belphemur"
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG APP_PATH=/usr/local/bin/CBZOptimizer ARG APP_PATH=/usr/local/bin/CBZOptimizer
ENV USER=abc ENV USER=abc
ENV CONFIG_FOLDER=/config ENV CONFIG_FOLDER=/config
ENV PUID=99 ENV PUID=99
ENV DEBIAN_FRONTEND=noninteractive # libwebp-tools (cwebp) is installed via apk below into /usr/bin; point go-webpbin
# at it directly so it doesn't try to download a glibc prebuilt binary.
ENV VENDOR_PATH=/usr/bin
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN adduser \
--mount=type=cache,target=/var/lib/apt,sharing=locked \ -S \
apt-get update && apt-get install -y --no-install-recommends adduser && \ -D \
addgroup --system users && \ -h "${CONFIG_FOLDER}" \
adduser \ -u "${PUID}" \
--system \ -G users \
--home "${CONFIG_FOLDER}" \ -s /bin/bash \
--uid "${PUID}" \ "${USER}"
--ingroup users \
--disabled-password \
"${USER}" && \
apt-get purge -y --auto-remove adduser
COPY ${TARGETPLATFORM}/CBZOptimizer ${APP_PATH} COPY ${TARGETPLATFORM}/CBZOptimizer ${APP_PATH}
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN apk add --no-cache \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get full-upgrade -y && \
apt-get install -y --no-install-recommends \
bash \ bash \
ca-certificates \ ca-certificates \
bash-completion && \ bash-completion \
libwebp-tools && \
chmod +x ${APP_PATH} && \ chmod +x ${APP_PATH} && \
${APP_PATH} completion bash > /etc/bash_completion.d/CBZOptimizer.bash ${APP_PATH} completion bash > /etc/bash_completion.d/CBZOptimizer.bash && \
mkdir -p "${CONFIG_FOLDER}" && \
chown -R "${PUID}":users "${CONFIG_FOLDER}"
USER ${USER} USER ${USER}
+11
View File
@@ -78,6 +78,16 @@ Watch a folder for new CBZ/CBR files and optimize them automatically:
cbzconverter watch [folder] --quality 85 --override --format webp --split cbzconverter watch [folder] --quality 85 --override --format webp --split
``` ```
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, 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: Or with Docker:
```sh ```sh
@@ -94,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` - Can be specified as: `--format webp`, `-f webp`, or `--format=webp`
- Case-insensitive: `webp`, `WEBP`, and `WebP` are all valid - 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. - `--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. - `--log`, `-l`: Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'. Default is info.
## Logging ## Logging
+25 -2
View File
@@ -41,6 +41,9 @@ func init() {
// Setup common flags (format, quality, override, split, timeout) with viper binding // Setup common flags (format, quality, override, split, timeout) with viper binding
setupCommonFlags(command, &converterType, 85, true, false, true) 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) AddCommand(command)
} }
func WatchCommand(_ *cobra.Command, args []string) error { func WatchCommand(_ *cobra.Command, args []string) error {
@@ -64,6 +67,8 @@ func WatchCommand(_ *cobra.Command, args []string) error {
timeout := viper.GetDuration("timeout") timeout := viper.GetDuration("timeout")
backfill := viper.GetBool("backfill")
converterType := constant.FindConversionFormat(viper.GetString("format")) converterType := constant.FindConversionFormat(viper.GetString("format"))
chapterConverter, err := converter.Get(converterType) chapterConverter, err := converter.Get(converterType)
if err != nil { if err != nil {
@@ -74,7 +79,7 @@ func WatchCommand(_ *cobra.Command, args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to prepare converter: %w", err) 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() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@@ -100,10 +105,17 @@ func WatchCommand(_ *cobra.Command, args []string) error {
debouncer := newEventDebouncer(debounceDelay, queue.Enqueue) debouncer := newEventDebouncer(debounceDelay, queue.Enqueue)
defer debouncer.Stop() defer debouncer.Stop()
// Note: existing archives already present under path when the watch
// 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 { if err := addRecursiveWatch(watcher, path); err != nil {
return fmt.Errorf("failed to watch path %s: %w", path, err) return fmt.Errorf("failed to watch path %s: %w", path, err)
} }
backfillExistingArchives(path, debouncer.Trigger) maybeBackfillExistingArchives(backfill, path, debouncer.Trigger)
for { for {
select { select {
@@ -194,6 +206,17 @@ func backfillExistingArchives(rootPath string, process func(path string)) {
} }
} }
// maybeBackfillExistingArchives runs backfillExistingArchives against
// rootPath only when enabled is true. It exists as its own function (rather
// than inlining the `if` check at the call site) so the gating decision used
// by WatchCommand can be exercised directly in tests.
func maybeBackfillExistingArchives(enabled bool, rootPath string, process func(path string)) {
if !enabled {
return
}
backfillExistingArchives(rootPath, process)
}
func shouldProcessWatchEvent(event fsnotify.Event) bool { func shouldProcessWatchEvent(event fsnotify.Event) bool {
return event.Has(fsnotify.Create) || event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) return event.Has(fsnotify.Create) || event.Has(fsnotify.Write) || event.Has(fsnotify.Rename)
} }
@@ -170,3 +170,35 @@ func TestOptimizeQueueSkipsMissingPath(t *testing.T) {
assert.EqualValues(t, 1, atomic.LoadInt32(&calls)) 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 TestMaybeBackfillExistingArchivesOnlyInvokedWhenRequested(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "chapter1.cbz"), []byte("data"), 0o644))
runBackfill := func(enabled bool) []string {
var found []string
var mu sync.Mutex
process := func(path string) {
mu.Lock()
defer mu.Unlock()
found = append(found, path)
}
// This is the exact same gating call WatchCommand makes based on the
// --backfill flag value.
maybeBackfillExistingArchives(enabled, 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 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 ## Key runtime requirements