summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-17 21:45:40 +0300
committerPaul Buetow <paul@buetow.org>2026-06-17 21:45:40 +0300
commiteb8cbbbcb9a613757ac4440246c43ebba6758afe (patch)
tree08ecc67cbc3d265d1e0379ba965017c06ca3a790
parent25aff490d739290602b28bdc53936dd3ea6ed939 (diff)
ln0 make --clean remove leftover staging directories
The --clean action deleted DIST_DIR but left behind the staging/backup directories the generation pipeline creates as siblings of DIST_DIR (.shuriken.<basename>.staging.* / .backup.*). Users expect --clean to remove all generation output, so extend it to also delete those. Cleanup runs after the validate_clean_dist_dir safety guard (8n0), so a dangerous DIST_DIR still aborts before any deletion. It only matches shuriken's own basename-specific staging/backup prefixes (the exact mktemp templates from config.staging.source.sh), uses nullglob so a missing match never expands to a literal pattern, and only removes directories. Unrelated dotfiles in the parent are never touched. Update test_clean to assert the staging/backup dirs are removed while unrelated entries survive, and document the behavior in the CLI usage text and README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--README.md9
-rwxr-xr-xbin/shuriken45
-rw-r--r--src/lib/action.source.sh44
-rw-r--r--src/lib/bootstrap.source.sh1
-rwxr-xr-xtests/cli.sh19
5 files changed, 113 insertions, 5 deletions
diff --git a/README.md b/README.md
index b853260..2757d08 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,9 @@ shuriken --version
* `--print-config` loads the config and overrides, validates basic config values,
and prints the effective configuration without writing output, running
ImageMagick, running tar, cleaning, or initializing.
-* `--clean` removes the configured output directory.
+* `--clean` removes the configured output directory and any leftover
+ `.shuriken.*.staging`/`.backup` directories the generation pipeline created as
+ siblings of `DIST_DIR` (e.g. from an interrupted run).
* `--version` prints the program version.
* `--config PATH` selects the config file for `--generate`,
`--refresh-splash`, `--dry-run`, `--print-config`, or `--clean`.
@@ -128,8 +130,9 @@ output (it is never written into `DIST_DIR`, so `--sync` does not deploy it), an
persists across runs even if `DIST_DIR` is removed or rebuilt. Because reading
EXIF from full-size originals is the slowest part of generation, keeping this
cache makes regenerating an album dramatically faster: an unchanged photo skips
-`identify` entirely. `--clean` removes `DIST_DIR` but leaves `cache/` in place;
-delete `cache/` by hand to force a full EXIF rebuild on the next run.
+`identify` entirely. `--clean` removes `DIST_DIR` (and any leftover staging
+directories) but leaves `cache/` in place; delete `cache/` by hand to force a
+full EXIF rebuild on the next run.
Pass `--force` with `--generate` to rebuild all generated image artifacts and
re-read every photo's EXIF from scratch (it clears `cache/` once up front, then
diff --git a/bin/shuriken b/bin/shuriken
index 3025da3..86317e3 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -135,6 +135,7 @@ usage() {
$0 --dry-run [--config PATH] [OPTIONS]
$0 --print-config [--config PATH] [OPTIONS]
$0 --clean [--config PATH] [OPTIONS]
+ (removes DIST_DIR and any leftover .shuriken.*.staging/backup dirs)
$0 --version
$0 --init
@@ -5459,6 +5460,44 @@ log_configured_action() {
log_verbose "Effective force generation setting: $SHURIKEN_FORCE_GENERATE"
}
+# Remove leftover generation staging/backup directories for DIST_DIR (ln0).
+#
+# The generation pipeline (config.staging.source.sh) stages output in sibling
+# directories of DIST_DIR named via mktemp templates
+# ".shuriken.<basename>.staging.XXXXXX" and ".shuriken.<basename>.backup.XXXXXX"
+# in DIST_DIR's parent. A crash or kill can leave these behind, so --clean
+# removes them too -- otherwise "clean" would not actually clean all generation
+# output (Principle of Least Astonishment).
+#
+# Safety: callers MUST run validate_clean_dist_dir first so a dangerous DIST_DIR
+# aborts before any deletion. We only match shuriken's own, basename-specific
+# staging/backup prefixes (never a loose ".shuriken.*" or arbitrary dotfiles),
+# derive the parent exactly as the staging code does (dirname "$DIST_DIR"), and
+# use nullglob so a missing match never expands to a literal pattern to rm.
+clean_generation_staging_artifacts() {
+ local final_base final_parent artifact
+ local -a artifacts=()
+
+ final_base=$(basename "$DIST_DIR")
+ final_parent=$(dirname "$DIST_DIR")
+
+ # nullglob: a non-matching glob expands to nothing rather than to the
+ # literal pattern, so we never accidentally rm a path called "*".
+ shopt -s nullglob
+ artifacts=(
+ "$final_parent/.shuriken.$final_base.staging."*
+ "$final_parent/.shuriken.$final_base.backup."*
+ )
+ shopt -u nullglob
+
+ for artifact in "${artifacts[@]}"; do
+ if [ -d "$artifact" ]; then
+ log_info "Cleaning leftover staging directory $artifact"
+ rm -rf "$artifact"
+ fi
+ done
+}
+
run_configured_action() {
local rc_file
local -i status=0
@@ -5496,6 +5535,12 @@ run_configured_action() {
else
log_verbose "Output directory does not exist: $DIST_DIR"
fi
+
+ # Also remove any leftover staging/backup directories that the
+ # generation pipeline created as siblings of DIST_DIR. This stays
+ # behind the validate_clean_dist_dir guard above (so a dangerous
+ # DIST_DIR aborts before any deletion).
+ clean_generation_staging_artifacts
;;
--generate)
validate_generation_config
diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh
index 425e009..4ff5d68 100644
--- a/src/lib/action.source.sh
+++ b/src/lib/action.source.sh
@@ -98,6 +98,44 @@ log_configured_action() {
log_verbose "Effective force generation setting: $SHURIKEN_FORCE_GENERATE"
}
+# Remove leftover generation staging/backup directories for DIST_DIR (ln0).
+#
+# The generation pipeline (config.staging.source.sh) stages output in sibling
+# directories of DIST_DIR named via mktemp templates
+# ".shuriken.<basename>.staging.XXXXXX" and ".shuriken.<basename>.backup.XXXXXX"
+# in DIST_DIR's parent. A crash or kill can leave these behind, so --clean
+# removes them too -- otherwise "clean" would not actually clean all generation
+# output (Principle of Least Astonishment).
+#
+# Safety: callers MUST run validate_clean_dist_dir first so a dangerous DIST_DIR
+# aborts before any deletion. We only match shuriken's own, basename-specific
+# staging/backup prefixes (never a loose ".shuriken.*" or arbitrary dotfiles),
+# derive the parent exactly as the staging code does (dirname "$DIST_DIR"), and
+# use nullglob so a missing match never expands to a literal pattern to rm.
+clean_generation_staging_artifacts() {
+ local final_base final_parent artifact
+ local -a artifacts=()
+
+ final_base=$(basename "$DIST_DIR")
+ final_parent=$(dirname "$DIST_DIR")
+
+ # nullglob: a non-matching glob expands to nothing rather than to the
+ # literal pattern, so we never accidentally rm a path called "*".
+ shopt -s nullglob
+ artifacts=(
+ "$final_parent/.shuriken.$final_base.staging."*
+ "$final_parent/.shuriken.$final_base.backup."*
+ )
+ shopt -u nullglob
+
+ for artifact in "${artifacts[@]}"; do
+ if [ -d "$artifact" ]; then
+ log_info "Cleaning leftover staging directory $artifact"
+ rm -rf "$artifact"
+ fi
+ done
+}
+
run_configured_action() {
local rc_file
local -i status=0
@@ -135,6 +173,12 @@ run_configured_action() {
else
log_verbose "Output directory does not exist: $DIST_DIR"
fi
+
+ # Also remove any leftover staging/backup directories that the
+ # generation pipeline created as siblings of DIST_DIR. This stays
+ # behind the validate_clean_dist_dir guard above (so a dangerous
+ # DIST_DIR aborts before any deletion).
+ clean_generation_staging_artifacts
;;
--generate)
validate_generation_config
diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh
index 52c0599..43cef59 100644
--- a/src/lib/bootstrap.source.sh
+++ b/src/lib/bootstrap.source.sh
@@ -14,6 +14,7 @@ usage() {
$0 --dry-run [--config PATH] [OPTIONS]
$0 --print-config [--config PATH] [OPTIONS]
$0 --clean [--config PATH] [OPTIONS]
+ (removes DIST_DIR and any leftover .shuriken.*.staging/backup dirs)
$0 --version
$0 --init
diff --git a/tests/cli.sh b/tests/cli.sh
index 5a58384..ad2a1d7 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -253,21 +253,36 @@ test_just_install_and_deinstall_with_destdir() {
test::teardown
}
+# --clean removes DIST_DIR and any leftover staging/backup directories that the
+# generation pipeline created as siblings of DIST_DIR (ln0). Unrelated dotfiles
+# and other entries in the parent must survive: we only target shuriken's own
+# basename-specific ".shuriken.<basename>.staging.*"/".backup.*" prefixes.
test_clean() {
local staging_dir
+ local backup_dir
+ local unrelated_dir
+ local unrelated_file
test::setup
printf 'DIST_DIR=%q/dist\n' "$TEST_TMPDIR" \
> "$TEST_TMPDIR/shuriken.conf"
mkdir -p "$TEST_TMPDIR/dist"
staging_dir="$TEST_TMPDIR/.shuriken.dist.staging.manual"
- mkdir -p "$staging_dir"
+ backup_dir="$TEST_TMPDIR/.shuriken.dist.backup.manual"
+ unrelated_dir="$TEST_TMPDIR/.shuriken.other.staging.keep"
+ unrelated_file="$TEST_TMPDIR/.keep-me"
+ mkdir -p "$staging_dir" "$backup_dir" "$unrelated_dir"
+ touch "$unrelated_file"
(
cd "$TEST_TMPDIR"
"$TEST_SHURIKEN" --clean
test::assert_path_absent "$TEST_TMPDIR/dist"
- test::assert_dir_exists "$staging_dir"
+ test::assert_path_absent "$staging_dir"
+ test::assert_path_absent "$backup_dir"
+ # Unrelated entries (different basename / non-shuriken) must survive.
+ test::assert_dir_exists "$unrelated_dir"
+ test::assert_file_exists "$unrelated_file"
)
test::teardown
}