summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-05 23:08:29 +0300
committerPaul Buetow <paul@buetow.org>2026-06-05 23:08:29 +0300
commitcc0c1be7fb9587978e34f9c7aaa1d7e8f74dcab5 (patch)
treef55bf3b6781c06fc8975bbf83859f87fae125294
parent89aa9bd7348ab5eb2324cc46ee749246b6d0ae36 (diff)
Add rsync sync command
-rw-r--r--README.md33
-rwxr-xr-xbin/photoalbum113
-rw-r--r--src/photoalbum.default.conf9
-rwxr-xr-xsrc/photoalbum.sh113
-rwxr-xr-xtests/cli.sh169
-rwxr-xr-xtests/helpers.sh21
6 files changed, 451 insertions, 7 deletions
diff --git a/README.md b/README.md
index 5df5d71..52b0bd4 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ modern `magick` command and falls back to `convert` when needed.
photoalbum --init
photoalbum --generate [--config PATH] [OPTIONS]
photoalbum --refresh-splash [--config PATH] [OPTIONS]
+photoalbum --sync [--config PATH] [OPTIONS]
photoalbum --dry-run [--config PATH] [OPTIONS]
photoalbum --print-config [--config PATH] [OPTIONS]
photoalbum --clean [--config PATH] [OPTIONS]
@@ -51,6 +52,8 @@ photoalbum --version
default config. It refuses to overwrite an existing file.
* `--generate` builds the static album.
* `--refresh-splash` rewrites only the generated root splash page.
+* `--sync` publishes the generated output directory to configured rsync
+ destinations.
* `--dry-run` loads the config and overrides, validates the planned generation,
and prints the effective paths, image count, tarball plan, and generated file
plan without writing output or running ImageMagick or tar.
@@ -69,7 +72,7 @@ If the file is missing, run `photoalbum --init` first.
The config file is a Bash file with assignments such as `INCOMING_DIR`,
`DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`,
`IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`,
-`TARBALL_INCLUDE`, and `TAR_TIMEOUT`.
+`TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and `SYNC_DESTINATIONS`.
Before generating, `photoalbum` validates the loaded config and command-line
overrides. It checks required values, positive integer settings, `yes`/`no`
@@ -90,9 +93,10 @@ tarball filename uses `<timestamp>` as a placeholder so the output is stable.
`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`,
`THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`,
`RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `TARBALL_INCLUDE`,
-`TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, and `ORIGINAL_BASEPATH`.
-Scalar values use Bash `%q` quoting and `TAR_OPTS` is normalized to a Bash array
-assignment, so the output can be parsed by shell tooling. `--quiet` does not
+`TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, `SYNC_DELETE`,
+`SYNC_DESTINATIONS`, and `ORIGINAL_BASEPATH`. Scalar values use Bash `%q`
+quoting and `TAR_OPTS` and `SYNC_DESTINATIONS` are normalized to Bash array
+assignments, so the output can be parsed by shell tooling. `--quiet` does not
suppress this output, and `--verbose` does not add human-readable diagnostics to
it.
@@ -120,6 +124,11 @@ The following long options override config values:
| `--no-splash` | `SPLASH_PAGE=no` |
| `--tarball` | `TARBALL_INCLUDE=yes` |
| `--no-tarball` | `TARBALL_INCLUDE=no` |
+| `--sync-delete` | `SYNC_DELETE=yes` |
+| `--no-sync-delete` | `SYNC_DELETE=no` |
+
+Pass `--sync-destination DEST` one or more times with `--sync` to override the
+configured sync destinations for that run.
By default, the generated root `index.html` is a no-JavaScript splash page using
a randomly selected album photo. Set `SPLASH_PAGE=no` or pass `--no-splash` to
@@ -137,6 +146,20 @@ timestamps, and `--shuffle` preview order remain non-deterministic. Set
choices repeatable for stable tests or reproducible album builds. Use the same
seed and inputs to produce the same HTML.
+To publish generated output, configure destinations and run `photoalbum --sync`:
+
+```
+SYNC_DESTINATIONS=(
+ admin@fishfinger.buetow.org:/var/www/htdocs/example.org/
+ admin@blowfish.buetow.org:/var/www/htdocs/example.org/
+)
+```
+
+`--sync` runs `rsync -av --delete "$DIST_DIR/" "$destination"` for each
+destination by default. The trailing slash on `DIST_DIR/` means the generated
+contents are copied into the target directory. Set `SYNC_DELETE=no` or pass
+`--no-sync-delete` to omit `--delete`.
+
`--dry-run`, `--print-config`, and `--refresh-splash` accept the same override
options as `--generate`. `--clean` accepts the same override options, but only
`--dist` changes what it removes.
@@ -163,7 +186,7 @@ value to a positive integer to adjust the limit for large images or archives.
pictures and adjust `DIST_DIR`, `TITLE`, or template settings as needed.
3. Run `photoalbum --dry-run` to inspect the planned generation.
4. Run `photoalbum --generate` to generate the album.
-5. Distribute the `./dist` directory to a static web server.
+5. Run `photoalbum --sync` to publish `./dist`, or distribute it manually.
6. Run `photoalbum --clean` to remove the generated output.
## HTML templates
diff --git a/bin/photoalbum b/bin/photoalbum
index d872179..6566c69 100755
--- a/bin/photoalbum
+++ b/bin/photoalbum
@@ -23,6 +23,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
RANDOM_SEED
SHUFFLE
SPLASH_PAGE
+ SYNC_DELETE
TARBALL_INCLUDE
)
declare -Ar CLI_OPTION_KIND=(
@@ -42,6 +43,9 @@ declare -Ar CLI_OPTION_KIND=(
[--no-splash]=flag
[--tarball]=flag
[--no-tarball]=flag
+ [--sync-delete]=flag
+ [--no-sync-delete]=flag
+ [--sync-destination]=value
[--verbose]=output
[--quiet]=output
[--version]=action
@@ -49,6 +53,7 @@ declare -Ar CLI_OPTION_KIND=(
[--clean]=action
[--generate]=action
[--refresh-splash]=action
+ [--sync]=action
[--dry-run]=action
[--print-config]=action
)
@@ -64,6 +69,8 @@ declare -Ar CLI_OPTION_VALUE=(
[--no-splash]=no
[--tarball]=yes
[--no-tarball]=no
+ [--sync-delete]=yes
+ [--no-sync-delete]=no
[--verbose]=verbose
[--quiet]=quiet
)
@@ -83,9 +90,12 @@ declare -Ar CLI_OPTION_CONFIG_TARGET=(
[--no-splash]=SPLASH_PAGE
[--tarball]=TARBALL_INCLUDE
[--no-tarball]=TARBALL_INCLUDE
+ [--sync-delete]=SYNC_DELETE
+ [--no-sync-delete]=SYNC_DELETE
)
declare -Ar CLI_OPTION_ARGUMENT=(
[--config]=path
+ [--sync-destination]=destination
)
usage() {
@@ -93,6 +103,7 @@ usage() {
Usage:
$0 --generate [--config PATH] [OPTIONS]
$0 --refresh-splash [--config PATH] [OPTIONS]
+ $0 --sync [--config PATH] [OPTIONS]
$0 --dry-run [--config PATH] [OPTIONS]
$0 --print-config [--config PATH] [OPTIONS]
$0 --clean [--config PATH] [OPTIONS]
@@ -116,6 +127,9 @@ usage() {
--no-shuffle
--tarball
--no-tarball
+ --sync-destination DEST
+ --sync-delete
+ --no-sync-delete
--verbose
--quiet
USAGE
@@ -407,6 +421,29 @@ resolve_tar_opts() {
fi
}
+resolve_sync_destinations() {
+ local -n destinations_ref="$1"; shift
+ local destinations_decl
+
+ destinations_ref=()
+
+ if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
+ return
+ fi
+
+ case "$destinations_decl" in
+ declare\ -a*\ SYNC_DESTINATIONS=*)
+ destinations_ref=("${SYNC_DESTINATIONS[@]}")
+ ;;
+ *)
+ if [ -n "${SYNC_DESTINATIONS:-}" ]; then
+ # shellcheck disable=SC2034
+ read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
+ fi
+ ;;
+ esac
+}
+
_html_escape() {
local text="$1"; shift
@@ -2040,8 +2077,10 @@ print_shell_array_assignment() {
print_config() {
local -a tar_opts=()
+ local -a sync_destinations=()
resolve_tar_opts tar_opts
+ resolve_sync_destinations sync_destinations
print_shell_assignment CONFIG_SOURCE "${PHOTOALBUM_CONFIG_SOURCE:-}"
print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
@@ -2060,9 +2099,28 @@ print_config() {
print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}"
print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT"
print_shell_array_assignment TAR_OPTS "${tar_opts[@]}"
+ print_shell_assignment SYNC_DELETE "${SYNC_DELETE:-yes}"
+ print_shell_array_assignment SYNC_DESTINATIONS "${sync_destinations[@]}"
print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}"
}
+sync_dist() {
+ local destination
+ local -a rsync_args=(-av)
+ local -a sync_destinations=()
+
+ resolve_sync_destinations sync_destinations
+
+ if [ "${SYNC_DELETE:-yes}" = yes ]; then
+ rsync_args+=(--delete)
+ fi
+
+ for destination in "${sync_destinations[@]}"; do
+ log_info "Syncing $DIST_DIR/ to $destination"
+ rsync "${rsync_args[@]}" "$DIST_DIR/" "$destination"
+ done
+}
+
existing_parent_dir() {
local -r path="$1"; shift
local existing_parent
@@ -2256,12 +2314,16 @@ apply_config_defaults() {
RANDOM_SEED="${RANDOM_SEED:-}"
SHUFFLE="${SHUFFLE:-no}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
+ SYNC_DELETE="${SYNC_DELETE:-yes}"
TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}"
TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}"
TAR_TIMEOUT="${TAR_TIMEOUT:-120}"
if ! declare -p TAR_OPTS >/dev/null 2>&1; then
TAR_OPTS=(-c)
fi
+ if ! declare -p SYNC_DESTINATIONS >/dev/null 2>&1; then
+ SYNC_DESTINATIONS=()
+ fi
}
option_value() {
@@ -2291,6 +2353,10 @@ apply_cli_overrides() {
for config_target in "${CLI_CONFIG_OVERRIDE_TARGETS[@]}"; do
apply_cli_override "$config_target"
done
+
+ if (( ${#cli_sync_destinations[@]} > 0 )); then
+ SYNC_DESTINATIONS=("${cli_sync_destinations[@]}")
+ fi
}
config_error() {
@@ -2493,9 +2559,42 @@ validate_generation_config() {
validate_print_config() {
local -a tar_opts=()
+ local -a sync_destinations=()
validate_common_config
resolve_tar_opts tar_opts
+ resolve_sync_destinations sync_destinations
+ validate_yes_no_config_var SYNC_DELETE
+}
+
+validate_sync_destinations() {
+ local -a sync_destinations=()
+
+ resolve_sync_destinations sync_destinations
+
+ if (( ${#sync_destinations[@]} == 0 )); then
+ config_error 'SYNC_DESTINATIONS must contain at least one destination'
+ fi
+}
+
+validate_rsync() {
+ if command -v rsync >/dev/null 2>&1; then
+ return
+ fi
+
+ config_error 'rsync is required to sync generated output'
+}
+
+validate_sync_config() {
+ require_config_var DIST_DIR
+ validate_yes_no_config_var SYNC_DELETE
+ validate_sync_destinations
+
+ if [[ ! -d "$DIST_DIR" || ! -r "$DIST_DIR" || ! -x "$DIST_DIR" ]]; then
+ config_error "DIST_DIR $DIST_DIR must be a readable directory"
+ fi
+
+ validate_rsync
}
set_cli_option_value() {
@@ -2503,6 +2602,12 @@ set_cli_option_value() {
local -r value="$1"; shift
local config_target
+ if [ "$option" = --sync-destination ]; then
+ cli_sync_destinations+=("$value")
+ has_config_overrides='yes'
+ return
+ fi
+
config_target="${CLI_OPTION_CONFIG_TARGET[$option]:-}"
if [ -n "$config_target" ]; then
cli_overrides["$config_target"]="$value"
@@ -2622,6 +2727,7 @@ log_configured_action() {
log_verbose "Effective tar timeout: ${TAR_TIMEOUT:-120}s"
log_verbose "Effective splash page setting: ${SPLASH_PAGE:-yes}"
log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
+ log_verbose "Effective sync delete setting: ${SYNC_DELETE:-yes}"
}
run_configured_action() {
@@ -2648,6 +2754,10 @@ run_configured_action() {
validate_refresh_splash_config
refresh_splash
;;
+ --sync)
+ validate_sync_config
+ sync_dist
+ ;;
--dry-run)
validate_generation_config no
dry_run
@@ -2664,7 +2774,7 @@ run_action() {
--version|--init)
run_simple_action
;;
- --clean|--generate|--refresh-splash|--dry-run|--print-config)
+ --clean|--generate|--refresh-splash|--sync|--dry-run|--print-config)
run_configured_action
;;
*)
@@ -2679,6 +2789,7 @@ main() {
local config_file=''
local has_config_overrides='no'
local -A cli_overrides=()
+ local -a cli_sync_destinations=()
if (( $# == 0 )); then
usage
diff --git a/src/photoalbum.default.conf b/src/photoalbum.default.conf
index 6ce1f6d..6fad282 100644
--- a/src/photoalbum.default.conf
+++ b/src/photoalbum.default.conf
@@ -32,6 +32,15 @@ TARBALL_SUFFIX=.tar
TAR_TIMEOUT=120
TAR_OPTS=(-c)
+# Destinations for `photoalbum --sync`. Each destination is passed to rsync
+# with DIST_DIR/ as the source, so the generated contents are copied into the
+# target directory. Sync deletes removed files from destinations by default.
+SYNC_DELETE=yes
+SYNC_DESTINATIONS=(
+# admin@fishfinger.buetow.org:/var/www/htdocs/example.org/
+# admin@blowfish.buetow.org:/var/www/htdocs/example.org/
+)
+
# Some debugging options
#set -e
#set -x
diff --git a/src/photoalbum.sh b/src/photoalbum.sh
index 0184a22..c215c01 100755
--- a/src/photoalbum.sh
+++ b/src/photoalbum.sh
@@ -23,6 +23,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
RANDOM_SEED
SHUFFLE
SPLASH_PAGE
+ SYNC_DELETE
TARBALL_INCLUDE
)
declare -Ar CLI_OPTION_KIND=(
@@ -42,6 +43,9 @@ declare -Ar CLI_OPTION_KIND=(
[--no-splash]=flag
[--tarball]=flag
[--no-tarball]=flag
+ [--sync-delete]=flag
+ [--no-sync-delete]=flag
+ [--sync-destination]=value
[--verbose]=output
[--quiet]=output
[--version]=action
@@ -49,6 +53,7 @@ declare -Ar CLI_OPTION_KIND=(
[--clean]=action
[--generate]=action
[--refresh-splash]=action
+ [--sync]=action
[--dry-run]=action
[--print-config]=action
)
@@ -64,6 +69,8 @@ declare -Ar CLI_OPTION_VALUE=(
[--no-splash]=no
[--tarball]=yes
[--no-tarball]=no
+ [--sync-delete]=yes
+ [--no-sync-delete]=no
[--verbose]=verbose
[--quiet]=quiet
)
@@ -83,9 +90,12 @@ declare -Ar CLI_OPTION_CONFIG_TARGET=(
[--no-splash]=SPLASH_PAGE
[--tarball]=TARBALL_INCLUDE
[--no-tarball]=TARBALL_INCLUDE
+ [--sync-delete]=SYNC_DELETE
+ [--no-sync-delete]=SYNC_DELETE
)
declare -Ar CLI_OPTION_ARGUMENT=(
[--config]=path
+ [--sync-destination]=destination
)
usage() {
@@ -93,6 +103,7 @@ usage() {
Usage:
$0 --generate [--config PATH] [OPTIONS]
$0 --refresh-splash [--config PATH] [OPTIONS]
+ $0 --sync [--config PATH] [OPTIONS]
$0 --dry-run [--config PATH] [OPTIONS]
$0 --print-config [--config PATH] [OPTIONS]
$0 --clean [--config PATH] [OPTIONS]
@@ -116,6 +127,9 @@ usage() {
--no-shuffle
--tarball
--no-tarball
+ --sync-destination DEST
+ --sync-delete
+ --no-sync-delete
--verbose
--quiet
USAGE
@@ -407,6 +421,29 @@ resolve_tar_opts() {
fi
}
+resolve_sync_destinations() {
+ local -n destinations_ref="$1"; shift
+ local destinations_decl
+
+ destinations_ref=()
+
+ if ! destinations_decl=$(declare -p SYNC_DESTINATIONS 2>/dev/null); then
+ return
+ fi
+
+ case "$destinations_decl" in
+ declare\ -a*\ SYNC_DESTINATIONS=*)
+ destinations_ref=("${SYNC_DESTINATIONS[@]}")
+ ;;
+ *)
+ if [ -n "${SYNC_DESTINATIONS:-}" ]; then
+ # shellcheck disable=SC2034
+ read -r -a destinations_ref <<< "$SYNC_DESTINATIONS"
+ fi
+ ;;
+ esac
+}
+
_html_escape() {
local text="$1"; shift
@@ -2040,8 +2077,10 @@ print_shell_array_assignment() {
print_config() {
local -a tar_opts=()
+ local -a sync_destinations=()
resolve_tar_opts tar_opts
+ resolve_sync_destinations sync_destinations
print_shell_assignment CONFIG_SOURCE "${PHOTOALBUM_CONFIG_SOURCE:-}"
print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
@@ -2060,9 +2099,28 @@ print_config() {
print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}"
print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT"
print_shell_array_assignment TAR_OPTS "${tar_opts[@]}"
+ print_shell_assignment SYNC_DELETE "${SYNC_DELETE:-yes}"
+ print_shell_array_assignment SYNC_DESTINATIONS "${sync_destinations[@]}"
print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}"
}
+sync_dist() {
+ local destination
+ local -a rsync_args=(-av)
+ local -a sync_destinations=()
+
+ resolve_sync_destinations sync_destinations
+
+ if [ "${SYNC_DELETE:-yes}" = yes ]; then
+ rsync_args+=(--delete)
+ fi
+
+ for destination in "${sync_destinations[@]}"; do
+ log_info "Syncing $DIST_DIR/ to $destination"
+ rsync "${rsync_args[@]}" "$DIST_DIR/" "$destination"
+ done
+}
+
existing_parent_dir() {
local -r path="$1"; shift
local existing_parent
@@ -2256,12 +2314,16 @@ apply_config_defaults() {
RANDOM_SEED="${RANDOM_SEED:-}"
SHUFFLE="${SHUFFLE:-no}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
+ SYNC_DELETE="${SYNC_DELETE:-yes}"
TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}"
TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}"
TAR_TIMEOUT="${TAR_TIMEOUT:-120}"
if ! declare -p TAR_OPTS >/dev/null 2>&1; then
TAR_OPTS=(-c)
fi
+ if ! declare -p SYNC_DESTINATIONS >/dev/null 2>&1; then
+ SYNC_DESTINATIONS=()
+ fi
}
option_value() {
@@ -2291,6 +2353,10 @@ apply_cli_overrides() {
for config_target in "${CLI_CONFIG_OVERRIDE_TARGETS[@]}"; do
apply_cli_override "$config_target"
done
+
+ if (( ${#cli_sync_destinations[@]} > 0 )); then
+ SYNC_DESTINATIONS=("${cli_sync_destinations[@]}")
+ fi
}
config_error() {
@@ -2493,9 +2559,42 @@ validate_generation_config() {
validate_print_config() {
local -a tar_opts=()
+ local -a sync_destinations=()
validate_common_config
resolve_tar_opts tar_opts
+ resolve_sync_destinations sync_destinations
+ validate_yes_no_config_var SYNC_DELETE
+}
+
+validate_sync_destinations() {
+ local -a sync_destinations=()
+
+ resolve_sync_destinations sync_destinations
+
+ if (( ${#sync_destinations[@]} == 0 )); then
+ config_error 'SYNC_DESTINATIONS must contain at least one destination'
+ fi
+}
+
+validate_rsync() {
+ if command -v rsync >/dev/null 2>&1; then
+ return
+ fi
+
+ config_error 'rsync is required to sync generated output'
+}
+
+validate_sync_config() {
+ require_config_var DIST_DIR
+ validate_yes_no_config_var SYNC_DELETE
+ validate_sync_destinations
+
+ if [[ ! -d "$DIST_DIR" || ! -r "$DIST_DIR" || ! -x "$DIST_DIR" ]]; then
+ config_error "DIST_DIR $DIST_DIR must be a readable directory"
+ fi
+
+ validate_rsync
}
set_cli_option_value() {
@@ -2503,6 +2602,12 @@ set_cli_option_value() {
local -r value="$1"; shift
local config_target
+ if [ "$option" = --sync-destination ]; then
+ cli_sync_destinations+=("$value")
+ has_config_overrides='yes'
+ return
+ fi
+
config_target="${CLI_OPTION_CONFIG_TARGET[$option]:-}"
if [ -n "$config_target" ]; then
cli_overrides["$config_target"]="$value"
@@ -2622,6 +2727,7 @@ log_configured_action() {
log_verbose "Effective tar timeout: ${TAR_TIMEOUT:-120}s"
log_verbose "Effective splash page setting: ${SPLASH_PAGE:-yes}"
log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
+ log_verbose "Effective sync delete setting: ${SYNC_DELETE:-yes}"
}
run_configured_action() {
@@ -2648,6 +2754,10 @@ run_configured_action() {
validate_refresh_splash_config
refresh_splash
;;
+ --sync)
+ validate_sync_config
+ sync_dist
+ ;;
--dry-run)
validate_generation_config no
dry_run
@@ -2664,7 +2774,7 @@ run_action() {
--version|--init)
run_simple_action
;;
- --clean|--generate|--refresh-splash|--dry-run|--print-config)
+ --clean|--generate|--refresh-splash|--sync|--dry-run|--print-config)
run_configured_action
;;
*)
@@ -2679,6 +2789,7 @@ main() {
local config_file=''
local has_config_overrides='no'
local -A cli_overrides=()
+ local -a cli_sync_destinations=()
if (( $# == 0 )); then
usage
diff --git a/tests/cli.sh b/tests/cli.sh
index d6e2787..905f6a4 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -1082,6 +1082,8 @@ TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
TAR_TIMEOUT=120
TAR_OPTS=( -c )
+SYNC_DELETE=yes
+SYNC_DESTINATIONS=( )
ORIGINAL_BASEPATH=''
EOF
)
@@ -1219,6 +1221,8 @@ TARBALL_INCLUDE=no
TARBALL_SUFFIX=.tar
TAR_TIMEOUT=120
TAR_OPTS=( -c )
+SYNC_DELETE=yes
+SYNC_DESTINATIONS=( )
ORIGINAL_BASEPATH=''
EOF
)
@@ -1258,6 +1262,8 @@ TARBALL_INCLUDE=no
TARBALL_SUFFIX=.tar
TAR_TIMEOUT=120
TAR_OPTS=( -c )
+SYNC_DELETE=yes
+SYNC_DESTINATIONS=( )
ORIGINAL_BASEPATH=''
EOF
)
@@ -1322,6 +1328,8 @@ TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
TAR_TIMEOUT=120
TAR_OPTS=( -c )
+SYNC_DELETE=yes
+SYNC_DESTINATIONS=( )
ORIGINAL_BASEPATH=''
EOF
)
@@ -3057,6 +3065,154 @@ test_generate_handles_space_and_underscore_names_distinctly() {
test::teardown
}
+test_sync_uses_config_destinations_with_delete() {
+ local config_file
+ local dist_dir
+ local fake_bin
+ local rsync_log
+ local rsync_output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ dist_dir="$TEST_TMPDIR/dist"
+ rsync_log="$TEST_TMPDIR/rsync.log"
+
+ test::install_rsync_spy "$fake_bin"
+ mkdir -p "$dist_dir"
+ printf 'generated\n' > "$dist_dir/index.html"
+ {
+ printf 'DIST_DIR=%q\n' "$dist_dir"
+ printf 'SYNC_DESTINATIONS=(\n'
+ printf ' %q\n' \
+ 'admin@fishfinger.buetow.org:/var/www/htdocs/example.org/'
+ printf ' %q\n' \
+ 'admin@blowfish.buetow.org:/var/www/htdocs/example.org/'
+ printf ')\n'
+ } > "$config_file"
+
+ (
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_RSYNC_LOG="$rsync_log" \
+ "$TEST_PHOTOALBUM" --sync
+ )
+
+ rsync_output=$(<"$rsync_log")
+ test::assert_contains $'argc=4\narg0=-av\narg1=--delete' \
+ "$rsync_output"
+ test::assert_contains "arg2=$dist_dir/" "$rsync_output"
+ test::assert_contains \
+ 'arg3=admin@fishfinger.buetow.org:/var/www/htdocs/example.org/' \
+ "$rsync_output"
+ test::assert_contains \
+ 'arg3=admin@blowfish.buetow.org:/var/www/htdocs/example.org/' \
+ "$rsync_output"
+ test::teardown
+}
+
+test_sync_cli_destinations_override_config_without_delete() {
+ local config_file
+ local dist_dir
+ local fake_bin
+ local rsync_log
+ local rsync_output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ dist_dir="$TEST_TMPDIR/dist"
+ rsync_log="$TEST_TMPDIR/rsync.log"
+
+ test::install_rsync_spy "$fake_bin"
+ mkdir -p "$dist_dir"
+ printf 'generated\n' > "$dist_dir/index.html"
+ {
+ printf 'DIST_DIR=%q\n' "$dist_dir"
+ printf 'SYNC_DESTINATIONS=(%q)\n' \
+ 'admin@config.example:/var/www/config/'
+ } > "$config_file"
+
+ (
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_RSYNC_LOG="$rsync_log" \
+ "$TEST_PHOTOALBUM" \
+ --sync \
+ --no-sync-delete \
+ --sync-destination 'admin@one.example:/var/www/one/' \
+ --sync-destination 'admin@two.example:/var/www/two/'
+ )
+
+ rsync_output=$(<"$rsync_log")
+ test::assert_contains $'argc=3\narg0=-av' "$rsync_output"
+ test::assert_not_contains '--delete' "$rsync_output"
+ test::assert_not_contains 'admin@config.example' "$rsync_output"
+ test::assert_contains "arg1=$dist_dir/" "$rsync_output"
+ test::assert_contains 'arg2=admin@one.example:/var/www/one/' \
+ "$rsync_output"
+ test::assert_contains 'arg2=admin@two.example:/var/www/two/' \
+ "$rsync_output"
+ test::teardown
+}
+
+test_sync_rejects_empty_destinations() {
+ local config_file
+ local dist_dir
+ local fake_bin
+ local output
+ local rsync_log
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+ dist_dir="$TEST_TMPDIR/dist"
+ rsync_log="$TEST_TMPDIR/rsync.log"
+
+ test::install_rsync_spy "$fake_bin"
+ mkdir -p "$dist_dir"
+ printf 'DIST_DIR=%q\n' "$dist_dir" > "$config_file"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_RSYNC_LOG="$rsync_log" \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --sync
+ )
+
+ test::assert_contains \
+ 'ERROR: SYNC_DESTINATIONS must contain at least one destination' \
+ "$output"
+ test::assert_path_absent "$rsync_log"
+ test::teardown
+}
+
+test_sync_rejects_missing_dist() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_rsync_spy "$fake_bin"
+ {
+ printf 'DIST_DIR=%q\n' "$TEST_TMPDIR/missing-dist"
+ printf 'SYNC_DESTINATIONS=(%q)\n' \
+ 'admin@example.org:/var/www/example.org/'
+ } > "$config_file"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_RSYNC_LOG="$TEST_TMPDIR/rsync.log" \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --sync
+ )
+
+ test::assert_contains \
+ "ERROR: DIST_DIR $TEST_TMPDIR/missing-dist must be a readable directory" \
+ "$output"
+ test::assert_path_absent "$TEST_TMPDIR/rsync.log"
+ test::teardown
+}
+
test_positional_commands_fail_without_deprecation() {
local output
local old_command
@@ -3113,6 +3269,7 @@ test_missing_option_values_fail() {
--maxpreviews
--image-jobs
--random-seed
+ --sync-destination
)
for option in "${value_options[@]}"; do
@@ -3343,6 +3500,18 @@ main() {
'--generate handles spaces and underscores distinctly' \
test_generate_handles_space_and_underscore_names_distinctly
test::run_case \
+ '--sync uses config destinations with delete' \
+ test_sync_uses_config_destinations_with_delete
+ test::run_case \
+ '--sync CLI destinations override config without delete' \
+ test_sync_cli_destinations_override_config_without_delete
+ test::run_case \
+ '--sync rejects empty destinations' \
+ test_sync_rejects_empty_destinations
+ test::run_case \
+ '--sync rejects missing dist' \
+ test_sync_rejects_missing_dist
+ test::run_case \
'positional commands fail without deprecation output' \
test_positional_commands_fail_without_deprecation
test::run_case \
diff --git a/tests/helpers.sh b/tests/helpers.sh
index 6f6d507..155cfc0 100755
--- a/tests/helpers.sh
+++ b/tests/helpers.sh
@@ -479,6 +479,27 @@ TAR
chmod 0755 "$bin_dir/tar"
}
+test::install_rsync_spy() {
+ local -r bin_dir="$1"; shift
+
+ mkdir -p "$bin_dir"
+
+ cat > "$bin_dir/rsync" <<'RSYNC'
+#!/usr/bin/env bash
+set -euo pipefail
+
+{
+ printf 'argc=%s\n' "$#"
+ i=0
+ for arg in "$@"; do
+ printf 'arg%s=%q\n' "$i" "$arg"
+ i=$(( i + 1 ))
+ done
+} >> "$TEST_RSYNC_LOG"
+RSYNC
+ chmod 0755 "$bin_dir/rsync"
+}
+
test::install_hanging_tar() {
local -r bin_dir="$1"; shift