summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-28 09:19:33 +0300
committerPaul Buetow <paul@buetow.org>2026-06-28 09:19:33 +0300
commit86c354932e698d52f23e65a1dafb1b7250f4c1b9 (patch)
tree2b67fdfab41fab1036c55e91bd2ddf72c25829dd
parent94479238557adcab33a95fefbf752a3742b8a8ba (diff)
Centralize DIST_DIR-derived paths via working_dir/exif_cache_dir helpers
The EXIF cache dir ($(dirname "$DIST_DIR")/cache/exif) was recomputed inline, byte for byte, in both cached_photo_identify_output (read/write) and clear_exif_cache (--force/--clean removal). The plain parent of DIST_DIR (dirname "$DIST_DIR") was likewise recomputed in metadata-cache and in action.source.sh's staging-artifact cleanup. Extract two helpers computed once from DIST_DIR: - working_dir() in config.source.sh (next to existing_parent_dir, the other DIST_DIR-parent resolver): plain `dirname "$DIST_DIR"`. - exif_cache_dir() in metadata-cache.source.sh (owns the EXIF cache): `working_dir()/cache/exif`. Route cached_photo_identify_output, clear_exif_cache, and clean_generation_staging_artifacts through them so the cache reader and the cleaner can never drift to different directories. Paths are byte-identical to the prior inline code (dirname semantics, cache/exif suffix, trailing-slash and relative/absolute handling all preserved). Stale "recompute the cache dir" comments removed; helpers document the path once. Add a unit test asserting the helpers agree and resolve beside dist for several DIST_DIR shapes. Broader DIST_DIR parameterization of leaf pipeline helpers was intentionally left out of scope (only the duplicated path computation is centralized here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rwxr-xr-xbin/shuriken50
-rw-r--r--src/lib/action.source.sh7
-rw-r--r--src/lib/config.source.sh14
-rw-r--r--src/lib/metadata-cache.source.sh29
-rwxr-xr-xtests/cli.sh28
5 files changed, 108 insertions, 20 deletions
diff --git a/bin/shuriken b/bin/shuriken
index a85c44e..0a4450d 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -2047,6 +2047,22 @@ camera_label_from_make_model() {
# dependency, it does not affect availability. Behaviour and signatures are
# unchanged by the move.
+# The volatile EXIF cache directory: ./cache/exif parallel to ./dist. Computed
+# once here (task pr0) from working_dir() (the parent of DIST_DIR) so the path is
+# defined in exactly ONE place. It was previously recomputed inline -- byte for
+# byte identically -- in both cached_photo_identify_output (read/write) and
+# clear_exif_cache (remove for --force/--clean); a single source for it keeps
+# those two in lockstep so the reader and the cleaner can never drift apart and
+# point at different directories. The path is deliberately a sibling of DIST_DIR
+# (working_dir()/cache/exif): the staging dir is a sibling of the final dist, so
+# this resolves to the working dir in both staging and direct contexts, lives
+# outside dist (surviving a fresh/cleared dist and never deployed), and lets an
+# unchanged photo skip the slow `identify -verbose` on every regenerate.
+# printf (no echo) keeps it safe under `set -euo pipefail`.
+exif_cache_dir() {
+ printf '%s\n' "$(working_dir)/cache/exif"
+}
+
# Build the cache signature line ("<photo>:<size>:<mtime>") used to decide
# whether a cache entry is still valid for the source file. Kept private to this
# module alongside its only consumers, plus the stats test that pre-seeds caches.
@@ -2086,12 +2102,11 @@ cached_photo_identify_output() {
local current_signature
local identify_status
- # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist
- # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is
- # the working dir in both staging and direct contexts). Keeping it outside
- # dist means it survives a fresh/cleared dist and is never deployed, so an
- # unchanged photo skips the slow `identify -verbose` on every regenerate.
- cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+ # Resolve the volatile EXIF cache dir via the shared exif_cache_dir() helper
+ # (see its definition above for why it sits parallel to ./dist and survives a
+ # cleared dist) so the reader/writer here and clear_exif_cache below always
+ # agree on the same directory.
+ cache_dir="$(exif_cache_dir)"
cache_file="$cache_dir/$photo.txt"
current_signature=$(photo_cache_signature "$photo" "$photo_path")
@@ -2188,7 +2203,7 @@ photo_exif_values_to() {
# lifecycle counterpart of cached_photo_identify_output above, so the cache's
# creation and destruction now live in the same module.
clear_exif_cache() {
- local -r cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+ local -r cache_dir="$(exif_cache_dir)"
log_verbose "Force generation; clearing EXIF cache $cache_dir"
rm -rf "$cache_dir"
@@ -5517,6 +5532,20 @@ existing_parent_dir() {
printf '%s\n' "$existing_parent"
}
+# The generation "working" directory: the parent of DIST_DIR. Several DIST_DIR
+# derived paths hang off this single parent -- the staging/backup sibling dirs
+# (config.staging, action) and the volatile EXIF cache (metadata-cache) all live
+# beside the final dist. Centralised here (task pr0), next to existing_parent_dir
+# (the other DIST_DIR-parent resolver), so the plain `dirname "$DIST_DIR"` is
+# computed in exactly one place instead of being recomputed at each call site.
+# Plain dirname (NOT existing_parent_dir): callers that previously inlined
+# `dirname "$DIST_DIR"` get a byte-identical result, including any trailing-slash
+# or relative-vs-absolute behaviour dirname already produced. printf (no echo) so
+# it stays safe under `set -euo pipefail`.
+working_dir() {
+ printf '%s\n' "$(dirname "$DIST_DIR")"
+}
+
resolve_config_file() {
local -r config_file="${1:-}"
@@ -6662,14 +6691,15 @@ log_configured_action() {
# 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.
+# derive the parent via working_dir() (the shared plain `dirname "$DIST_DIR"`,
+# exactly the parent the staging code stages into), 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")
+ final_parent=$(working_dir)
# nullglob: a non-matching glob expands to nothing rather than to the
# literal pattern, so we never accidentally rm a path called "*".
diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh
index c667519..a038fa7 100644
--- a/src/lib/action.source.sh
+++ b/src/lib/action.source.sh
@@ -179,14 +179,15 @@ log_configured_action() {
# 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.
+# derive the parent via working_dir() (the shared plain `dirname "$DIST_DIR"`,
+# exactly the parent the staging code stages into), 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")
+ final_parent=$(working_dir)
# nullglob: a non-matching glob expands to nothing rather than to the
# literal pattern, so we never accidentally rm a path called "*".
diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh
index caa2fe3..c676899 100644
--- a/src/lib/config.source.sh
+++ b/src/lib/config.source.sh
@@ -11,6 +11,20 @@ existing_parent_dir() {
printf '%s\n' "$existing_parent"
}
+# The generation "working" directory: the parent of DIST_DIR. Several DIST_DIR
+# derived paths hang off this single parent -- the staging/backup sibling dirs
+# (config.staging, action) and the volatile EXIF cache (metadata-cache) all live
+# beside the final dist. Centralised here (task pr0), next to existing_parent_dir
+# (the other DIST_DIR-parent resolver), so the plain `dirname "$DIST_DIR"` is
+# computed in exactly one place instead of being recomputed at each call site.
+# Plain dirname (NOT existing_parent_dir): callers that previously inlined
+# `dirname "$DIST_DIR"` get a byte-identical result, including any trailing-slash
+# or relative-vs-absolute behaviour dirname already produced. printf (no echo) so
+# it stays safe under `set -euo pipefail`.
+working_dir() {
+ printf '%s\n' "$(dirname "$DIST_DIR")"
+}
+
resolve_config_file() {
local -r config_file="${1:-}"
diff --git a/src/lib/metadata-cache.source.sh b/src/lib/metadata-cache.source.sh
index aa580b6..2a367c7 100644
--- a/src/lib/metadata-cache.source.sh
+++ b/src/lib/metadata-cache.source.sh
@@ -10,6 +10,22 @@
# dependency, it does not affect availability. Behaviour and signatures are
# unchanged by the move.
+# The volatile EXIF cache directory: ./cache/exif parallel to ./dist. Computed
+# once here (task pr0) from working_dir() (the parent of DIST_DIR) so the path is
+# defined in exactly ONE place. It was previously recomputed inline -- byte for
+# byte identically -- in both cached_photo_identify_output (read/write) and
+# clear_exif_cache (remove for --force/--clean); a single source for it keeps
+# those two in lockstep so the reader and the cleaner can never drift apart and
+# point at different directories. The path is deliberately a sibling of DIST_DIR
+# (working_dir()/cache/exif): the staging dir is a sibling of the final dist, so
+# this resolves to the working dir in both staging and direct contexts, lives
+# outside dist (surviving a fresh/cleared dist and never deployed), and lets an
+# unchanged photo skip the slow `identify -verbose` on every regenerate.
+# printf (no echo) keeps it safe under `set -euo pipefail`.
+exif_cache_dir() {
+ printf '%s\n' "$(working_dir)/cache/exif"
+}
+
# Build the cache signature line ("<photo>:<size>:<mtime>") used to decide
# whether a cache entry is still valid for the source file. Kept private to this
# module alongside its only consumers, plus the stats test that pre-seeds caches.
@@ -49,12 +65,11 @@ cached_photo_identify_output() {
local current_signature
local identify_status
- # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist
- # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is
- # the working dir in both staging and direct contexts). Keeping it outside
- # dist means it survives a fresh/cleared dist and is never deployed, so an
- # unchanged photo skips the slow `identify -verbose` on every regenerate.
- cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+ # Resolve the volatile EXIF cache dir via the shared exif_cache_dir() helper
+ # (see its definition above for why it sits parallel to ./dist and survives a
+ # cleared dist) so the reader/writer here and clear_exif_cache below always
+ # agree on the same directory.
+ cache_dir="$(exif_cache_dir)"
cache_file="$cache_dir/$photo.txt"
current_signature=$(photo_cache_signature "$photo" "$photo_path")
@@ -151,7 +166,7 @@ photo_exif_values_to() {
# lifecycle counterpart of cached_photo_identify_output above, so the cache's
# creation and destruction now live in the same module.
clear_exif_cache() {
- local -r cache_dir="$(dirname "$DIST_DIR")/cache/exif"
+ local -r cache_dir="$(exif_cache_dir)"
log_verbose "Force generation; clearing EXIF cache $cache_dir"
rm -rf "$cache_dir"
diff --git a/tests/cli.sh b/tests/cli.sh
index 2280a7e..c8cbd36 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -6396,6 +6396,31 @@ test::source_shuriken_lib() {
source <(sed '$d' "$TEST_SHURIKEN")
}
+# pr0: the EXIF cache dir is computed once via exif_cache_dir() (= working_dir()/
+# cache/exif, where working_dir() is dirname "$DIST_DIR"). Both the cache
+# reader/writer and the --force/--clean cleaner route through this single helper,
+# so they can never point at different directories. Assert the helpers agree and
+# resolve to the documented "sibling of dist" path for a few DIST_DIR shapes.
+test_exif_cache_dir_resolves_beside_dist() {
+ test::setup
+ test::source_shuriken_lib
+
+ DIST_DIR='/home/u/site/dist'
+ test "$(working_dir)" = '/home/u/site'
+ test "$(exif_cache_dir)" = '/home/u/site/cache/exif'
+
+ # Relative DIST_DIR keeps dirname semantics (parent is ".").
+ DIST_DIR='dist'
+ test "$(working_dir)" = '.'
+ test "$(exif_cache_dir)" = './cache/exif'
+
+ # A trailing slash is collapsed exactly as dirname collapses it.
+ DIST_DIR='a/b/dist/'
+ test "$(exif_cache_dir)" = 'a/b/cache/exif'
+
+ test::teardown
+}
+
test_stats_aggregates_synthetic_exif_fixtures() {
local fixture
@@ -7188,6 +7213,9 @@ main() {
'--generate reuses cached EXIF details unless forced' \
test_generate_reuses_cached_exif_details_unless_forced
test::run_case \
+ 'EXIF cache dir resolves beside dist' \
+ test_exif_cache_dir_resolves_beside_dist
+ test::run_case \
'stats aggregate synthetic EXIF fixtures' \
test_stats_aggregates_synthetic_exif_fixtures
test::run_case \