summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/photoalbum205
-rwxr-xr-xsrc/photoalbum.sh205
-rwxr-xr-xtests/cli.sh156
-rwxr-xr-xtests/helpers.sh59
4 files changed, 589 insertions, 36 deletions
diff --git a/bin/photoalbum b/bin/photoalbum
index 03d017b..a3ff361 100755
--- a/bin/photoalbum
+++ b/bin/photoalbum
@@ -113,7 +113,8 @@ tarball() {
find "$DIST_DIR" -maxdepth 1 -type f -name '*.tar' -delete
base=$(basename "$INCOMING_DIR")
- echo "Creating tarball $DIST_DIR/$tarball_name from $INCOMING_DIR"
+ echo "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
+ "from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
tar "$tar_opts" -f "$DIST_DIR/$tarball_name" "$base"
@@ -145,6 +146,17 @@ _css_string_escape() {
printf '%s\n' "$text"
}
+_display_path() {
+ local -r path="$1"; shift
+ local -r final_dist="${PHOTOALBUM_FINAL_DIST_DIR:-}"
+
+ if [[ -n "$final_dist" && "$path" == "$DIST_DIR"* ]]; then
+ printf '%s%s\n' "$final_dist" "${path#"$DIST_DIR"}"
+ else
+ printf '%s\n' "$path"
+ fi
+}
+
template() {
local -r template_name="$1"; shift
local -r html="$1"; shift
@@ -168,7 +180,7 @@ template() {
local thumbs_dir_html
local title_html
- echo "Generating $dist_html/$html"
+ echo "Generating $(_display_path "$dist_html")/$html"
mkdir -p "$dist_html"
animation_class_html=$(_html_escape "${animation_class:-}")
@@ -224,7 +236,7 @@ cleanphotos() {
continue
fi
- echo "Cleaning up $photo"
+ echo "Cleaning up $(_display_path "$photo")"
for sub in thumbs blurs photos; do
if [ -f "$DIST_DIR/$sub/$basename" ]; then
rm -v "$DIST_DIR/$sub/$basename"
@@ -244,11 +256,11 @@ scalephotos() {
mkdir -p "$dirname"
if [ -f "$destphoto" ]; then
- echo "Already exists: $destphoto"
+ echo "Already exists: $(_display_path "$destphoto")"
continue
fi
- echo "Processing $photo to $destphoto"
+ echo "Processing $photo to $(_display_path "$destphoto")"
if [ -n "${HEIGHT:-}" ]; then
# Scale down size.
imagemagick \
@@ -385,12 +397,12 @@ albumhtml() {
if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
&& -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
- echo "Already exists: $DIST_DIR/$thumbs_dir/$photo and" \
- "$DIST_DIR/$blurs_dir/$photo"
+ echo "Already exists: $(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
+ "$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
else
dirname="$DIST_DIR/$thumbs_dir"
mkdir -p "$dirname"
- echo "Creating thumb $DIST_DIR/$thumbs_dir/$photo"
+ echo "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
# Double the height, as CSS scales images based on boxing too.
height=$(( THUMBHEIGHT * 2 ))
imagemagick \
@@ -400,7 +412,7 @@ albumhtml() {
dirname="$DIST_DIR/$blurs_dir"
mkdir -p "$dirname"
- echo "Creating blur $DIST_DIR/$blurs_dir/$photo"
+ echo "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
imagemagick \
"$DIST_DIR/$thumbs_dir/$photo" \
-flip \
@@ -465,7 +477,8 @@ randomphoto() {
)
if (( ${#photos[@]} == 0 )); then
- echo "ERROR: No photos found in $DIST_DIR/$photos_dir" >&2
+ echo "ERROR: No photos found in" \
+ "$(_display_path "$DIST_DIR/$photos_dir")" >&2
return 1
fi
@@ -503,6 +516,168 @@ generate() {
fi
}
+existing_parent_dir() {
+ local -r path="$1"; shift
+ local existing_parent
+
+ existing_parent=$(dirname "$path")
+
+ while [ ! -e "$existing_parent" ]; do
+ existing_parent=$(dirname "$existing_parent")
+ done
+
+ printf '%s\n' "$existing_parent"
+}
+
+generation_staging_dir() {
+ local -r final_dist="$1"; shift
+ local final_base
+ local staging_parent
+
+ final_base=$(basename "$final_dist")
+ staging_parent=$(existing_parent_dir "$final_dist")
+
+ mktemp -d "$staging_parent/.photoalbum.$final_base.staging.XXXXXX"
+}
+
+prepare_generation_staging_dir() {
+ local -r final_dist="$1"; shift
+ local -r staging_dir="$1"; shift
+ local cache_dir
+
+ for cache_dir in photos thumbs blurs; do
+ if [ -d "$final_dist/$cache_dir" ]; then
+ if ! mkdir -p "$staging_dir/$cache_dir"; then
+ return 1
+ fi
+ if ! cp -a "$final_dist/$cache_dir/." "$staging_dir/$cache_dir/"; then
+ return 1
+ fi
+ fi
+ done
+}
+
+cleanup_generation_staging_dir() {
+ if [ -n "${PHOTOALBUM_ACTIVE_STAGING_DIR:-}" ]; then
+ rm -rf "$PHOTOALBUM_ACTIVE_STAGING_DIR"
+ PHOTOALBUM_ACTIVE_STAGING_DIR=''
+ fi
+}
+
+clear_generation_staging_traps() {
+ trap - EXIT INT TERM
+}
+
+ignore_generation_staging_interrupts() {
+ trap '' INT TERM
+}
+
+replace_dist_with_staging() {
+ local -r staging_dir="$1"; shift
+ local -r final_dist="$1"; shift
+ local backup_dist=''
+ local backup_parent=''
+ local final_base
+ local final_parent
+ local staging_parent
+ local -i status=0
+
+ final_base=$(basename "$final_dist")
+ final_parent=$(dirname "$final_dist")
+ staging_parent=$(existing_parent_dir "$final_dist")
+
+ if ! mkdir -p "$final_parent"; then
+ return 1
+ fi
+
+ if [ -e "$final_dist" ]; then
+ if ! backup_parent=$(
+ mktemp -d "$staging_parent/.photoalbum.$final_base.backup.XXXXXX"
+ ); then
+ return 1
+ fi
+ backup_dist="$backup_parent/dist"
+
+ if ! mv "$final_dist" "$backup_dist"; then
+ rm -rf "$backup_parent"
+ return 1
+ fi
+ fi
+
+ if mv "$staging_dir" "$final_dist"; then
+ if [ -n "$backup_parent" ]; then
+ rm -rf "$backup_parent"
+ fi
+ return 0
+ else
+ status=$?
+ fi
+
+ if [ -n "$backup_dist" ] && [ -e "$backup_dist" ]; then
+ if [ -e "$final_dist" ] && ! rm -rf "$final_dist"; then
+ echo "ERROR: Failed to restore $final_dist from $backup_dist" >&2
+ return "$status"
+ fi
+ if ! mv "$backup_dist" "$final_dist"; then
+ echo "ERROR: Failed to restore $final_dist from $backup_dist" >&2
+ return "$status"
+ fi
+ rm -rf "$backup_parent"
+ fi
+
+ return "$status"
+}
+
+generate_staged() {
+ local -r final_dist="$DIST_DIR"
+ local staging_dir
+ local -i status=0
+
+ staging_dir=$(generation_staging_dir "$final_dist")
+ PHOTOALBUM_ACTIVE_STAGING_DIR="$staging_dir"
+ trap cleanup_generation_staging_dir EXIT
+ trap 'cleanup_generation_staging_dir; exit 130' INT
+ trap 'cleanup_generation_staging_dir; exit 143' TERM
+
+ if ! prepare_generation_staging_dir "$final_dist" "$staging_dir"; then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return 1
+ fi
+
+ set +e
+ (
+ set -e
+ PHOTOALBUM_FINAL_DIST_DIR="$final_dist"
+ export PHOTOALBUM_FINAL_DIST_DIR
+ DIST_DIR="$staging_dir"
+ generate
+ )
+ status=$?
+ set -e
+
+ if (( status != 0 )); then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return "$status"
+ fi
+
+ ignore_generation_staging_interrupts
+ set +e
+ replace_dist_with_staging "$staging_dir" "$final_dist"
+ status=$?
+ set -e
+
+ if (( status != 0 )); then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return "$status"
+ fi
+
+ PHOTOALBUM_ACTIVE_STAGING_DIR=''
+ clear_generation_staging_traps
+}
+
resolve_config_file() {
local -r config_file="${1:-}"
@@ -619,7 +794,6 @@ validate_yes_no_config_var() {
validate_dist_dir() {
local existing_parent
- local parent_dir
if [ -e "$DIST_DIR" ]; then
if [ ! -d "$DIST_DIR" ]; then
@@ -632,12 +806,7 @@ validate_dist_dir() {
return
fi
- parent_dir=$(dirname "$DIST_DIR")
- existing_parent="$parent_dir"
-
- while [ ! -e "$existing_parent" ]; do
- existing_parent=$(dirname "$existing_parent")
- done
+ existing_parent=$(existing_parent_dir "$DIST_DIR")
if [ ! -d "$existing_parent" ]; then
config_error "DIST_DIR parent $existing_parent must be a directory"
@@ -854,7 +1023,7 @@ main() {
;;
--generate)
validate_generation_config
- generate
+ generate_staged
;;
esac
;;
diff --git a/src/photoalbum.sh b/src/photoalbum.sh
index 9cb17a8..af520f5 100755
--- a/src/photoalbum.sh
+++ b/src/photoalbum.sh
@@ -113,7 +113,8 @@ tarball() {
find "$DIST_DIR" -maxdepth 1 -type f -name '*.tar' -delete
base=$(basename "$INCOMING_DIR")
- echo "Creating tarball $DIST_DIR/$tarball_name from $INCOMING_DIR"
+ echo "Creating tarball $(_display_path "$DIST_DIR/$tarball_name")" \
+ "from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
tar "$tar_opts" -f "$DIST_DIR/$tarball_name" "$base"
@@ -145,6 +146,17 @@ _css_string_escape() {
printf '%s\n' "$text"
}
+_display_path() {
+ local -r path="$1"; shift
+ local -r final_dist="${PHOTOALBUM_FINAL_DIST_DIR:-}"
+
+ if [[ -n "$final_dist" && "$path" == "$DIST_DIR"* ]]; then
+ printf '%s%s\n' "$final_dist" "${path#"$DIST_DIR"}"
+ else
+ printf '%s\n' "$path"
+ fi
+}
+
template() {
local -r template_name="$1"; shift
local -r html="$1"; shift
@@ -168,7 +180,7 @@ template() {
local thumbs_dir_html
local title_html
- echo "Generating $dist_html/$html"
+ echo "Generating $(_display_path "$dist_html")/$html"
mkdir -p "$dist_html"
animation_class_html=$(_html_escape "${animation_class:-}")
@@ -224,7 +236,7 @@ cleanphotos() {
continue
fi
- echo "Cleaning up $photo"
+ echo "Cleaning up $(_display_path "$photo")"
for sub in thumbs blurs photos; do
if [ -f "$DIST_DIR/$sub/$basename" ]; then
rm -v "$DIST_DIR/$sub/$basename"
@@ -244,11 +256,11 @@ scalephotos() {
mkdir -p "$dirname"
if [ -f "$destphoto" ]; then
- echo "Already exists: $destphoto"
+ echo "Already exists: $(_display_path "$destphoto")"
continue
fi
- echo "Processing $photo to $destphoto"
+ echo "Processing $photo to $(_display_path "$destphoto")"
if [ -n "${HEIGHT:-}" ]; then
# Scale down size.
imagemagick \
@@ -385,12 +397,12 @@ albumhtml() {
if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \
&& -f "$DIST_DIR/$blurs_dir/$photo" ]]; then
- echo "Already exists: $DIST_DIR/$thumbs_dir/$photo and" \
- "$DIST_DIR/$blurs_dir/$photo"
+ echo "Already exists: $(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \
+ "$(_display_path "$DIST_DIR/$blurs_dir/$photo")"
else
dirname="$DIST_DIR/$thumbs_dir"
mkdir -p "$dirname"
- echo "Creating thumb $DIST_DIR/$thumbs_dir/$photo"
+ echo "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")"
# Double the height, as CSS scales images based on boxing too.
height=$(( THUMBHEIGHT * 2 ))
imagemagick \
@@ -400,7 +412,7 @@ albumhtml() {
dirname="$DIST_DIR/$blurs_dir"
mkdir -p "$dirname"
- echo "Creating blur $DIST_DIR/$blurs_dir/$photo"
+ echo "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")"
imagemagick \
"$DIST_DIR/$thumbs_dir/$photo" \
-flip \
@@ -465,7 +477,8 @@ randomphoto() {
)
if (( ${#photos[@]} == 0 )); then
- echo "ERROR: No photos found in $DIST_DIR/$photos_dir" >&2
+ echo "ERROR: No photos found in" \
+ "$(_display_path "$DIST_DIR/$photos_dir")" >&2
return 1
fi
@@ -503,6 +516,168 @@ generate() {
fi
}
+existing_parent_dir() {
+ local -r path="$1"; shift
+ local existing_parent
+
+ existing_parent=$(dirname "$path")
+
+ while [ ! -e "$existing_parent" ]; do
+ existing_parent=$(dirname "$existing_parent")
+ done
+
+ printf '%s\n' "$existing_parent"
+}
+
+generation_staging_dir() {
+ local -r final_dist="$1"; shift
+ local final_base
+ local staging_parent
+
+ final_base=$(basename "$final_dist")
+ staging_parent=$(existing_parent_dir "$final_dist")
+
+ mktemp -d "$staging_parent/.photoalbum.$final_base.staging.XXXXXX"
+}
+
+prepare_generation_staging_dir() {
+ local -r final_dist="$1"; shift
+ local -r staging_dir="$1"; shift
+ local cache_dir
+
+ for cache_dir in photos thumbs blurs; do
+ if [ -d "$final_dist/$cache_dir" ]; then
+ if ! mkdir -p "$staging_dir/$cache_dir"; then
+ return 1
+ fi
+ if ! cp -a "$final_dist/$cache_dir/." "$staging_dir/$cache_dir/"; then
+ return 1
+ fi
+ fi
+ done
+}
+
+cleanup_generation_staging_dir() {
+ if [ -n "${PHOTOALBUM_ACTIVE_STAGING_DIR:-}" ]; then
+ rm -rf "$PHOTOALBUM_ACTIVE_STAGING_DIR"
+ PHOTOALBUM_ACTIVE_STAGING_DIR=''
+ fi
+}
+
+clear_generation_staging_traps() {
+ trap - EXIT INT TERM
+}
+
+ignore_generation_staging_interrupts() {
+ trap '' INT TERM
+}
+
+replace_dist_with_staging() {
+ local -r staging_dir="$1"; shift
+ local -r final_dist="$1"; shift
+ local backup_dist=''
+ local backup_parent=''
+ local final_base
+ local final_parent
+ local staging_parent
+ local -i status=0
+
+ final_base=$(basename "$final_dist")
+ final_parent=$(dirname "$final_dist")
+ staging_parent=$(existing_parent_dir "$final_dist")
+
+ if ! mkdir -p "$final_parent"; then
+ return 1
+ fi
+
+ if [ -e "$final_dist" ]; then
+ if ! backup_parent=$(
+ mktemp -d "$staging_parent/.photoalbum.$final_base.backup.XXXXXX"
+ ); then
+ return 1
+ fi
+ backup_dist="$backup_parent/dist"
+
+ if ! mv "$final_dist" "$backup_dist"; then
+ rm -rf "$backup_parent"
+ return 1
+ fi
+ fi
+
+ if mv "$staging_dir" "$final_dist"; then
+ if [ -n "$backup_parent" ]; then
+ rm -rf "$backup_parent"
+ fi
+ return 0
+ else
+ status=$?
+ fi
+
+ if [ -n "$backup_dist" ] && [ -e "$backup_dist" ]; then
+ if [ -e "$final_dist" ] && ! rm -rf "$final_dist"; then
+ echo "ERROR: Failed to restore $final_dist from $backup_dist" >&2
+ return "$status"
+ fi
+ if ! mv "$backup_dist" "$final_dist"; then
+ echo "ERROR: Failed to restore $final_dist from $backup_dist" >&2
+ return "$status"
+ fi
+ rm -rf "$backup_parent"
+ fi
+
+ return "$status"
+}
+
+generate_staged() {
+ local -r final_dist="$DIST_DIR"
+ local staging_dir
+ local -i status=0
+
+ staging_dir=$(generation_staging_dir "$final_dist")
+ PHOTOALBUM_ACTIVE_STAGING_DIR="$staging_dir"
+ trap cleanup_generation_staging_dir EXIT
+ trap 'cleanup_generation_staging_dir; exit 130' INT
+ trap 'cleanup_generation_staging_dir; exit 143' TERM
+
+ if ! prepare_generation_staging_dir "$final_dist" "$staging_dir"; then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return 1
+ fi
+
+ set +e
+ (
+ set -e
+ PHOTOALBUM_FINAL_DIST_DIR="$final_dist"
+ export PHOTOALBUM_FINAL_DIST_DIR
+ DIST_DIR="$staging_dir"
+ generate
+ )
+ status=$?
+ set -e
+
+ if (( status != 0 )); then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return "$status"
+ fi
+
+ ignore_generation_staging_interrupts
+ set +e
+ replace_dist_with_staging "$staging_dir" "$final_dist"
+ status=$?
+ set -e
+
+ if (( status != 0 )); then
+ cleanup_generation_staging_dir
+ clear_generation_staging_traps
+ return "$status"
+ fi
+
+ PHOTOALBUM_ACTIVE_STAGING_DIR=''
+ clear_generation_staging_traps
+}
+
resolve_config_file() {
local -r config_file="${1:-}"
@@ -619,7 +794,6 @@ validate_yes_no_config_var() {
validate_dist_dir() {
local existing_parent
- local parent_dir
if [ -e "$DIST_DIR" ]; then
if [ ! -d "$DIST_DIR" ]; then
@@ -632,12 +806,7 @@ validate_dist_dir() {
return
fi
- parent_dir=$(dirname "$DIST_DIR")
- existing_parent="$parent_dir"
-
- while [ ! -e "$existing_parent" ]; do
- existing_parent=$(dirname "$existing_parent")
- done
+ existing_parent=$(existing_parent_dir "$DIST_DIR")
if [ ! -d "$existing_parent" ]; then
config_error "DIST_DIR parent $existing_parent must be a directory"
@@ -854,7 +1023,7 @@ main() {
;;
--generate)
validate_generation_config
- generate
+ generate_staged
;;
esac
;;
diff --git a/tests/cli.sh b/tests/cli.sh
index c998075..91de42a 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -87,15 +87,20 @@ test_init_existing_config_fails_without_overwrite() {
}
test_clean() {
+ local staging_dir
+
test::setup
printf 'DIST_DIR=%q/dist\n' "$TEST_TMPDIR" \
> "$TEST_TMPDIR/photoalbum.conf"
mkdir -p "$TEST_TMPDIR/dist"
+ staging_dir="$TEST_TMPDIR/.photoalbum.dist.staging.manual"
+ mkdir -p "$staging_dir"
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --clean
test::assert_path_absent "$TEST_TMPDIR/dist"
+ test::assert_dir_exists "$staging_dir"
)
test::teardown
}
@@ -727,6 +732,145 @@ test_integration_generates_album_outputs_and_cleans() {
test::teardown
}
+test_generate_replaces_dist_after_success() {
+ local config_file
+ local fake_bin
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Replace album' 40
+ mkdir -p "$TEST_TMPDIR/dist/html" "$TEST_TMPDIR/dist/photos"
+ printf 'stale\n' > "$TEST_TMPDIR/dist/stale-root-file"
+ printf 'stale\n' > "$TEST_TMPDIR/dist/html/stale.html"
+ printf 'stale\n' > "$TEST_TMPDIR/dist/photos/stale.jpg"
+
+ (
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_file_exists "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
+ test::assert_file_exists "$TEST_TMPDIR/dist/html/page-1.html"
+ test::assert_path_absent "$TEST_TMPDIR/dist/stale-root-file"
+ test::assert_path_absent "$TEST_TMPDIR/dist/html/stale.html"
+ test::assert_path_absent "$TEST_TMPDIR/dist/photos/stale.jpg"
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
+test_generate_imagemagick_failure_preserves_dist() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_failing_imagemagick "$fake_bin"
+ mkdir -p "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist"
+ printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
+ printf 'old dist\n' > "$TEST_TMPDIR/dist/index.html"
+ printf 'keep me\n' > "$TEST_TMPDIR/dist/sentinel"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Failing ImageMagick album' 40
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains 'simulated ImageMagick failure' "$output"
+ test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old dist'
+ test "$(cat "$TEST_TMPDIR/dist/sentinel")" = 'keep me'
+ test::assert_path_absent "$TEST_TMPDIR/dist/photos/01.jpg"
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
+test_generate_template_failure_preserves_dist() {
+ local config_file
+ local fake_bin
+ local output
+ local template_dir
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ template_dir="$TEST_TMPDIR/templates"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ cp -R "$TEST_REPO_ROOT/share/templates/default" "$template_dir"
+ printf 'return 42\n' > "$template_dir/preview.tmpl"
+ mkdir -p "$TEST_TMPDIR/dist"
+ printf 'old index\n' > "$TEST_TMPDIR/dist/index.html"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Failing template album' 40
+ printf 'TEMPLATE_DIR=%q\n' "$template_dir" >> "$config_file"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains 'Generating' "$output"
+ test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old index'
+ test::assert_path_absent "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
+test_generate_swap_failure_restores_dist() {
+ local config_file
+ local fake_bin
+ local mv_count_file
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ mv_count_file="$TEST_TMPDIR/mv-count"
+
+ test::install_fake_imagemagick "$fake_bin"
+ test::install_mv_spy "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ mkdir -p "$TEST_TMPDIR/dist"
+ printf 'old index\n' > "$TEST_TMPDIR/dist/index.html"
+ printf 'old sentinel\n' > "$TEST_TMPDIR/dist/sentinel"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Swap failure album' 40
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" \
+ TEST_MV_COUNT_FILE="$mv_count_file" \
+ TEST_FAIL_MV_ON=2 \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains 'simulated mv failure' "$output"
+ test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old index'
+ test "$(cat "$TEST_TMPDIR/dist/sentinel")" = 'old sentinel'
+ test::assert_path_absent "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
test_generate_missing_imagemagick_fails() {
local config_file
local output
@@ -1019,6 +1163,18 @@ main() {
'--generate creates output structure and --clean removes it' \
test_integration_generates_album_outputs_and_cleans
test::run_case \
+ '--generate replaces final dist after success' \
+ test_generate_replaces_dist_after_success
+ test::run_case \
+ '--generate ImageMagick failure preserves final dist' \
+ test_generate_imagemagick_failure_preserves_dist
+ test::run_case \
+ '--generate template failure preserves final dist' \
+ test_generate_template_failure_preserves_dist
+ test::run_case \
+ '--generate swap failure restores final dist' \
+ test_generate_swap_failure_restores_dist
+ test::run_case \
'--generate fails when ImageMagick is missing' \
test_generate_missing_imagemagick_fails
test::run_case \
diff --git a/tests/helpers.sh b/tests/helpers.sh
index 8590b36..f0ef852 100755
--- a/tests/helpers.sh
+++ b/tests/helpers.sh
@@ -164,6 +164,19 @@ test::assert_find_count() {
fi
}
+test::assert_no_staging_dirs() {
+ local -r dir="$1"; shift
+ local found
+
+ found=$(find "$dir" -type d -name '.photoalbum.*' -print -quit)
+
+ if [ -n "$found" ]; then
+ echo "FAIL: expected no staging directories under $dir" >&2
+ echo "found $found" >&2
+ exit 1
+ fi
+}
+
test::run_photoalbum() {
"$TEST_PHOTOALBUM" "$@" 2>&1
}
@@ -191,6 +204,52 @@ MAGICK
cp "$bin_dir/magick" "$bin_dir/convert"
}
+test::install_failing_imagemagick() {
+ local -r bin_dir="$1"; shift
+
+ mkdir -p "$bin_dir"
+
+ cat > "$bin_dir/magick" <<'MAGICK'
+#!/usr/bin/env bash
+set -euo pipefail
+
+echo 'simulated ImageMagick failure' >&2
+exit 42
+MAGICK
+ chmod 0755 "$bin_dir/magick"
+ cp "$bin_dir/magick" "$bin_dir/convert"
+}
+
+test::install_mv_spy() {
+ local -r bin_dir="$1"; shift
+ local real_mv
+
+ mkdir -p "$bin_dir"
+ real_mv=$(command -v mv)
+
+ cat > "$bin_dir/mv" <<MV
+#!/usr/bin/env bash
+set -euo pipefail
+
+count=0
+if [ -n "\${TEST_MV_COUNT_FILE:-}" ] && [ -f "\$TEST_MV_COUNT_FILE" ]; then
+ count=\$(<"\$TEST_MV_COUNT_FILE")
+fi
+count=\$(( count + 1 ))
+if [ -n "\${TEST_MV_COUNT_FILE:-}" ]; then
+ printf '%s\n' "\$count" > "\$TEST_MV_COUNT_FILE"
+fi
+
+if [[ -n "\${TEST_FAIL_MV_ON:-}" && "\$count" = "\$TEST_FAIL_MV_ON" ]]; then
+ echo 'simulated mv failure' >&2
+ exit 42
+fi
+
+"$real_mv" "\$@"
+MV
+ chmod 0755 "$bin_dir/mv"
+}
+
test::install_sort_spy() {
local -r bin_dir="$1"; shift
local real_sort