summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-11 22:31:55 +0300
committerPaul Buetow <paul@buetow.org>2026-06-11 22:31:55 +0300
commit44d3f7a689751633f8ac5f06b4ba0be8c056bb42 (patch)
tree8957632b4e4af3fec8724858f6196392dd06a05d /src/lib
parente6c32dd8d134b0f90a144fd415585c0b12ce0d74 (diff)
Split config module concerns for task 9m0
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/config.cli.source.sh109
-rw-r--r--src/lib/config.print.source.sh46
-rw-r--r--src/lib/config.source.sh629
-rw-r--r--src/lib/config.staging.source.sh216
-rw-r--r--src/lib/config.sync.source.sh16
-rw-r--r--src/lib/config.validate.source.sh237
6 files changed, 624 insertions, 629 deletions
diff --git a/src/lib/config.cli.source.sh b/src/lib/config.cli.source.sh
new file mode 100644
index 0000000..1df4b84
--- /dev/null
+++ b/src/lib/config.cli.source.sh
@@ -0,0 +1,109 @@
+option_value() {
+ local -r option="$1"; shift
+ local -r argument="${CLI_OPTION_ARGUMENT[$option]:-value}"
+
+ if (( $# == 0 )) || [ -z "$1" ]; then
+ printf 'Error: %s requires a %s\n' "$option" "$argument" >&2
+ usage
+ exit 1
+ fi
+
+ printf '%s\n' "$1"
+}
+
+apply_cli_override() {
+ local -r config_target="$1"; shift
+
+ if [[ -v "cli_overrides[$config_target]" ]]; then
+ printf -v "$config_target" '%s' "${cli_overrides[$config_target]}"
+ fi
+}
+
+apply_cli_overrides() {
+ local config_target
+
+ 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
+}
+
+set_cli_option_value() {
+ local -r option="$1"; shift
+ 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"
+ has_config_overrides='yes'
+ return
+ fi
+
+ printf -v "${CLI_OPTION_TARGET[$option]}" '%s' "$value"
+}
+
+set_cli_constant_option() {
+ local -r option="$1"; shift
+ local -r value="${CLI_OPTION_VALUE[$option]}"
+ local config_target
+
+ config_target="${CLI_OPTION_CONFIG_TARGET[$option]:-}"
+ if [ -n "$config_target" ]; then
+ cli_overrides["$config_target"]="$value"
+ has_config_overrides='yes'
+ return
+ fi
+
+ printf -v "${CLI_OPTION_TARGET[$option]}" '%s' "$value"
+}
+
+set_cli_action() {
+ local -r selected_action="$1"; shift
+
+ if [ -n "$action" ]; then
+ usage
+ exit 1
+ fi
+
+ action="$selected_action"
+}
+
+parse_cli_arguments() {
+ local option_arg
+ local option
+ local option_kind
+
+ while (( $# > 0 )); do
+ option="$1"
+ shift
+
+ option_kind="${CLI_OPTION_KIND[$option]:-}"
+ case "$option_kind" in
+ value)
+ option_arg=$(option_value "$option" "$@")
+ set_cli_option_value "$option" "$option_arg"
+ shift
+ ;;
+ flag|output)
+ set_cli_constant_option "$option"
+ ;;
+ action)
+ set_cli_action "$option"
+ ;;
+ *)
+ usage
+ exit 1
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/config.print.source.sh b/src/lib/config.print.source.sh
new file mode 100644
index 0000000..1950a26
--- /dev/null
+++ b/src/lib/config.print.source.sh
@@ -0,0 +1,46 @@
+print_shell_assignment() {
+ local -r name="$1"; shift
+ local -r value="$1"; shift
+
+ printf '%s=%q\n' "$name" "$value"
+}
+
+print_shell_array_assignment() {
+ local -r name="$1"; shift
+ local value
+
+ printf '%s=(' "$name"
+ for value in "$@"; do
+ printf ' %q' "$value"
+ done
+ printf ' )\n'
+}
+
+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 "${SHURIKEN_CONFIG_SOURCE:-}"
+ print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
+ print_shell_assignment DIST_DIR "$DIST_DIR"
+ print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR"
+ print_shell_assignment TITLE "$TITLE"
+ print_shell_assignment HEIGHT "${HEIGHT:-}"
+ print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
+ print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS"
+ print_shell_assignment IMAGE_JOBS "$IMAGE_JOBS"
+ print_shell_assignment IMAGEMAGICK_TIMEOUT "$IMAGEMAGICK_TIMEOUT"
+ print_shell_assignment RANDOM_SEED "${RANDOM_SEED:-}"
+ print_shell_assignment SHUFFLE "${SHUFFLE:-no}"
+ print_shell_assignment SPLASH_PAGE "${SPLASH_PAGE:-yes}"
+ print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}"
+ 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:-}"
+}
diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh
index 35624b7..348f167 100644
--- a/src/lib/config.source.sh
+++ b/src/lib/config.source.sh
@@ -1,67 +1,3 @@
-print_shell_assignment() {
- local -r name="$1"; shift
- local -r value="$1"; shift
-
- printf '%s=%q\n' "$name" "$value"
-}
-
-print_shell_array_assignment() {
- local -r name="$1"; shift
- local value
-
- printf '%s=(' "$name"
- for value in "$@"; do
- printf ' %q' "$value"
- done
- printf ' )\n'
-}
-
-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 "${SHURIKEN_CONFIG_SOURCE:-}"
- print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
- print_shell_assignment DIST_DIR "$DIST_DIR"
- print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR"
- print_shell_assignment TITLE "$TITLE"
- print_shell_assignment HEIGHT "${HEIGHT:-}"
- print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
- print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS"
- print_shell_assignment IMAGE_JOBS "$IMAGE_JOBS"
- print_shell_assignment IMAGEMAGICK_TIMEOUT "$IMAGEMAGICK_TIMEOUT"
- print_shell_assignment RANDOM_SEED "${RANDOM_SEED:-}"
- print_shell_assignment SHUFFLE "${SHUFFLE:-no}"
- print_shell_assignment SPLASH_PAGE "${SPLASH_PAGE:-yes}"
- print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}"
- 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
@@ -75,223 +11,6 @@ existing_parent_dir() {
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/.shuriken.$final_base.staging.XXXXXX"
-}
-
-prepare_generation_staging_dir() {
- local -r final_dist="$1"; shift
- local -r staging_dir="$1"; shift
- local cache_dir
-
- if [ "$SHURIKEN_FORCE_GENERATE" = yes ]; then
- log_verbose 'Force generation enabled; not reusing existing output cache'
- return
- fi
-
- for cache_dir in photos thumbs blurs .shuriken-cache; 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 "${SHURIKEN_ACTIVE_STAGING_DIR:-}" ]; then
- rm -rf "$SHURIKEN_ACTIVE_STAGING_DIR"
- SHURIKEN_ACTIVE_STAGING_DIR=''
- fi
-}
-
-terminate_process_tree() {
- local -r signal="$1"; shift
- local -r pid="$1"; shift
- local child
-
- if [ -z "$pid" ]; then
- return
- fi
-
- while IFS= read -r child; do
- if [ -n "$child" ]; then
- terminate_process_tree "$signal" "$child"
- fi
- done < <(pgrep -P "$pid" 2>/dev/null || true)
-
- kill "-$signal" "$pid" 2>/dev/null || true
-}
-
-wait_for_process_exit() {
- local -r pid="$1"; shift
- local -i attempts=0
-
- while kill -0 "$pid" 2>/dev/null; do
- if (( attempts >= 20 )); then
- return 1
- fi
- (( ++attempts ))
- sleep 0.05
- done
-}
-
-terminate_active_generation() {
- local -r pid="${SHURIKEN_ACTIVE_GENERATION_PID:-}"
-
- if [ -z "$pid" ]; then
- return
- fi
-
- terminate_process_tree TERM "$pid"
- if ! wait_for_process_exit "$pid"; then
- terminate_process_tree KILL "$pid"
- fi
- wait "$pid" 2>/dev/null || true
- SHURIKEN_ACTIVE_GENERATION_PID=''
-}
-
-handle_generation_staging_signal() {
- local -r status="$1"; shift
-
- clear_generation_staging_traps
- terminate_active_generation
- cleanup_generation_staging_dir
- exit "$status"
-}
-
-clear_generation_staging_traps() {
- trap - EXIT INT TERM HUP PIPE
-}
-
-ignore_generation_staging_interrupts() {
- trap '' INT TERM HUP PIPE
-}
-
-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/.shuriken.$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
- printf 'ERROR: Failed to restore %s from %s\n' \
- "$final_dist" "$backup_dist" >&2
- return "$status"
- fi
- if ! mv "$backup_dist" "$final_dist"; then
- printf 'ERROR: Failed to restore %s from %s\n' \
- "$final_dist" "$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")
- log_verbose "Effective output directory: $final_dist"
- log_verbose "Generation staging directory: $staging_dir"
- SHURIKEN_ACTIVE_STAGING_DIR="$staging_dir"
- trap cleanup_generation_staging_dir EXIT
- trap 'handle_generation_staging_signal 130' INT
- trap 'handle_generation_staging_signal 143' TERM
- trap 'handle_generation_staging_signal 129' HUP
- trap 'handle_generation_staging_signal 141' PIPE
-
- 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
- SHURIKEN_FINAL_DIST_DIR="$final_dist"
- export SHURIKEN_FINAL_DIST_DIR
- DIST_DIR="$staging_dir" generate
- ) &
- SHURIKEN_ACTIVE_GENERATION_PID=$!
- wait "$SHURIKEN_ACTIVE_GENERATION_PID"
- status=$?
- SHURIKEN_ACTIVE_GENERATION_PID=''
- 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
-
- SHURIKEN_ACTIVE_STAGING_DIR=''
- clear_generation_staging_traps
-}
-
resolve_config_file() {
local -r config_file="${1:-}"
@@ -329,351 +48,3 @@ apply_config_defaults() {
SYNC_DESTINATIONS=()
fi
}
-
-option_value() {
- local -r option="$1"; shift
- local -r argument="${CLI_OPTION_ARGUMENT[$option]:-value}"
-
- if (( $# == 0 )) || [ -z "$1" ]; then
- printf 'Error: %s requires a %s\n' "$option" "$argument" >&2
- usage
- exit 1
- fi
-
- printf '%s\n' "$1"
-}
-
-apply_cli_override() {
- local -r config_target="$1"; shift
-
- if [[ -v "cli_overrides[$config_target]" ]]; then
- printf -v "$config_target" '%s' "${cli_overrides[$config_target]}"
- fi
-}
-
-apply_cli_overrides() {
- local config_target
-
- 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() {
- local -r message="$1"; shift
-
- printf 'ERROR: %s\n' "$message" >&2
- return 1
-}
-
-require_config_var() {
- local -r name="$1"; shift
-
- if [ -z "${!name+x}" ] || [ -z "${!name}" ]; then
- config_error "$name must be set in shuriken configuration"
- fi
-}
-
-validate_positive_integer_config_var() {
- local -r name="$1"; shift
- local -r value="${!name}"
-
- if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
- config_error "$name must be a positive integer"
- fi
-}
-
-validate_optional_positive_integer_config_var() {
- local -r name="$1"; shift
-
- if [ -n "${!name}" ]; then
- validate_positive_integer_config_var "$name"
- fi
-}
-
-validate_yes_no_config_var() {
- local -r name="$1"; shift
- local -r value="${!name}"
-
- case "$value" in
- yes|no)
- ;;
- *)
- config_error "$name must be yes or no"
- ;;
- esac
-}
-
-validate_dist_dir() {
- local existing_parent
-
- if [ -e "$DIST_DIR" ]; then
- if [ ! -d "$DIST_DIR" ]; then
- config_error "DIST_DIR $DIST_DIR must be a directory"
- fi
- if [[ ! -w "$DIST_DIR" || ! -x "$DIST_DIR" ]]; then
- config_error "DIST_DIR $DIST_DIR must be writable"
- fi
-
- return
- fi
-
- existing_parent=$(existing_parent_dir "$DIST_DIR")
-
- if [ ! -d "$existing_parent" ]; then
- config_error "DIST_DIR parent $existing_parent must be a directory"
- fi
- if [[ ! -w "$existing_parent" || ! -x "$existing_parent" ]]; then
- config_error "DIST_DIR parent $existing_parent must be writable"
- fi
-}
-
-validate_template_dir_access() {
- if [[ ! -d "$TEMPLATE_DIR" || ! -r "$TEMPLATE_DIR" \
- || ! -x "$TEMPLATE_DIR" ]]; then
- config_error "TEMPLATE_DIR $TEMPLATE_DIR must be a readable directory"
- fi
-}
-
-validate_template_file() {
- local -r template_name="$1"; shift
-
- if [ ! -r "$TEMPLATE_DIR/$template_name.tmpl" ]; then
- config_error \
- "template file $TEMPLATE_DIR/$template_name.tmpl must be readable"
- fi
-}
-
-validate_template_dir() {
- local template_name
- local -a required_templates=(
- details
- footer
- header
- next
- prev
- preview
- redirect
- view
- )
-
- validate_template_dir_access
-
- if [ "${SPLASH_PAGE:-yes}" = yes ]; then
- required_templates+=(splash)
- fi
-
- for template_name in "${required_templates[@]}"; do
- validate_template_file "$template_name"
- done
-}
-
-validate_refresh_splash_config() {
- local required_var
- local -a required_vars=(
- TITLE
- DIST_DIR
- TEMPLATE_DIR
- )
-
- for required_var in "${required_vars[@]}"; do
- require_config_var "$required_var"
- done
-
- validate_yes_no_config_var SPLASH_PAGE
-
- if [ "${SPLASH_PAGE:-yes}" != yes ]; then
- config_error 'SPLASH_PAGE must be yes to refresh the splash page'
- fi
-
- validate_dist_dir
-
- validate_template_dir_access
- validate_template_file splash
-
- if [ ! -d "$DIST_DIR/photos" ]; then
- config_error "DIST_DIR photos directory $DIST_DIR/photos must exist"
- fi
-
- if [ ! -d "$DIST_DIR/blurs" ]; then
- config_error "DIST_DIR blurs directory $DIST_DIR/blurs must exist"
- fi
-}
-
-validate_imagemagick() {
- if command -v magick >/dev/null 2>&1; then
- return
- fi
- if command -v convert >/dev/null 2>&1; then
- return
- fi
-
- config_error 'ImageMagick is required; install magick or convert'
-}
-
-validate_common_config() {
- local required_var
- local -a required_vars=(
- TITLE
- THUMBHEIGHT
- MAXPREVIEWS
- IMAGE_JOBS
- INCOMING_DIR
- DIST_DIR
- TEMPLATE_DIR
- )
-
- for required_var in "${required_vars[@]}"; do
- require_config_var "$required_var"
- done
-
- validate_optional_positive_integer_config_var HEIGHT
- validate_positive_integer_config_var THUMBHEIGHT
- validate_positive_integer_config_var MAXPREVIEWS
- validate_positive_integer_config_var IMAGE_JOBS
- validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT
- validate_positive_integer_config_var TAR_TIMEOUT
- validate_yes_no_config_var SHUFFLE
- validate_yes_no_config_var SPLASH_PAGE
- validate_yes_no_config_var TARBALL_INCLUDE
-}
-
-validate_generation_config() {
- local -r require_imagemagick="${1:-yes}"
-
- validate_common_config
-
- if [ ! -d "$INCOMING_DIR" ]; then
- config_error "You have to create $INCOMING_DIR first"
- fi
- if [[ ! -r "$INCOMING_DIR" || ! -x "$INCOMING_DIR" ]]; then
- config_error "INCOMING_DIR $INCOMING_DIR must be readable"
- fi
-
- validate_dist_dir
- validate_template_dir
- if [ "$require_imagemagick" = yes ]; then
- validate_imagemagick
- fi
-}
-
-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() {
- local -r option="$1"; shift
- 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"
- has_config_overrides='yes'
- return
- fi
-
- printf -v "${CLI_OPTION_TARGET[$option]}" '%s' "$value"
-}
-
-set_cli_constant_option() {
- local -r option="$1"; shift
- local -r value="${CLI_OPTION_VALUE[$option]}"
- local config_target
-
- config_target="${CLI_OPTION_CONFIG_TARGET[$option]:-}"
- if [ -n "$config_target" ]; then
- cli_overrides["$config_target"]="$value"
- has_config_overrides='yes'
- return
- fi
-
- printf -v "${CLI_OPTION_TARGET[$option]}" '%s' "$value"
-}
-
-set_cli_action() {
- local -r selected_action="$1"; shift
-
- if [ -n "$action" ]; then
- usage
- exit 1
- fi
-
- action="$selected_action"
-}
-
-parse_cli_arguments() {
- local option_arg
- local option
- local option_kind
-
- while (( $# > 0 )); do
- option="$1"
- shift
-
- option_kind="${CLI_OPTION_KIND[$option]:-}"
- case "$option_kind" in
- value)
- option_arg=$(option_value "$option" "$@")
- set_cli_option_value "$option" "$option_arg"
- shift
- ;;
- flag|output)
- set_cli_constant_option "$option"
- ;;
- action)
- set_cli_action "$option"
- ;;
- *)
- usage
- exit 1
- ;;
- esac
- done
-}
diff --git a/src/lib/config.staging.source.sh b/src/lib/config.staging.source.sh
new file mode 100644
index 0000000..8a5f8f1
--- /dev/null
+++ b/src/lib/config.staging.source.sh
@@ -0,0 +1,216 @@
+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/.shuriken.$final_base.staging.XXXXXX"
+}
+
+prepare_generation_staging_dir() {
+ local -r final_dist="$1"; shift
+ local -r staging_dir="$1"; shift
+ local cache_dir
+
+ if [ "$SHURIKEN_FORCE_GENERATE" = yes ]; then
+ log_verbose 'Force generation enabled; not reusing existing output cache'
+ return
+ fi
+
+ for cache_dir in photos thumbs blurs .shuriken-cache; 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 "${SHURIKEN_ACTIVE_STAGING_DIR:-}" ]; then
+ rm -rf "$SHURIKEN_ACTIVE_STAGING_DIR"
+ SHURIKEN_ACTIVE_STAGING_DIR=''
+ fi
+}
+
+terminate_process_tree() {
+ local -r signal="$1"; shift
+ local -r pid="$1"; shift
+ local child
+
+ if [ -z "$pid" ]; then
+ return
+ fi
+
+ while IFS= read -r child; do
+ if [ -n "$child" ]; then
+ terminate_process_tree "$signal" "$child"
+ fi
+ done < <(pgrep -P "$pid" 2>/dev/null || true)
+
+ kill "-$signal" "$pid" 2>/dev/null || true
+}
+
+wait_for_process_exit() {
+ local -r pid="$1"; shift
+ local -i attempts=0
+
+ while kill -0 "$pid" 2>/dev/null; do
+ if (( attempts >= 20 )); then
+ return 1
+ fi
+ (( ++attempts ))
+ sleep 0.05
+ done
+}
+
+terminate_active_generation() {
+ local -r pid="${SHURIKEN_ACTIVE_GENERATION_PID:-}"
+
+ if [ -z "$pid" ]; then
+ return
+ fi
+
+ terminate_process_tree TERM "$pid"
+ if ! wait_for_process_exit "$pid"; then
+ terminate_process_tree KILL "$pid"
+ fi
+ wait "$pid" 2>/dev/null || true
+ SHURIKEN_ACTIVE_GENERATION_PID=''
+}
+
+handle_generation_staging_signal() {
+ local -r status="$1"; shift
+
+ clear_generation_staging_traps
+ terminate_active_generation
+ cleanup_generation_staging_dir
+ exit "$status"
+}
+
+clear_generation_staging_traps() {
+ trap - EXIT INT TERM HUP PIPE
+}
+
+ignore_generation_staging_interrupts() {
+ trap '' INT TERM HUP PIPE
+}
+
+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/.shuriken.$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
+ printf 'ERROR: Failed to restore %s from %s\n' \
+ "$final_dist" "$backup_dist" >&2
+ return "$status"
+ fi
+ if ! mv "$backup_dist" "$final_dist"; then
+ printf 'ERROR: Failed to restore %s from %s\n' \
+ "$final_dist" "$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")
+ log_verbose "Effective output directory: $final_dist"
+ log_verbose "Generation staging directory: $staging_dir"
+ SHURIKEN_ACTIVE_STAGING_DIR="$staging_dir"
+ trap cleanup_generation_staging_dir EXIT
+ trap 'handle_generation_staging_signal 130' INT
+ trap 'handle_generation_staging_signal 143' TERM
+ trap 'handle_generation_staging_signal 129' HUP
+ trap 'handle_generation_staging_signal 141' PIPE
+
+ 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
+ SHURIKEN_FINAL_DIST_DIR="$final_dist"
+ export SHURIKEN_FINAL_DIST_DIR
+ DIST_DIR="$staging_dir" generate
+ ) &
+ SHURIKEN_ACTIVE_GENERATION_PID=$!
+ wait "$SHURIKEN_ACTIVE_GENERATION_PID"
+ status=$?
+ SHURIKEN_ACTIVE_GENERATION_PID=''
+ 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
+
+ SHURIKEN_ACTIVE_STAGING_DIR=''
+ clear_generation_staging_traps
+}
diff --git a/src/lib/config.sync.source.sh b/src/lib/config.sync.source.sh
new file mode 100644
index 0000000..d2256d3
--- /dev/null
+++ b/src/lib/config.sync.source.sh
@@ -0,0 +1,16 @@
+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
+}
diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh
new file mode 100644
index 0000000..373f05d
--- /dev/null
+++ b/src/lib/config.validate.source.sh
@@ -0,0 +1,237 @@
+config_error() {
+ local -r message="$1"; shift
+
+ printf 'ERROR: %s\n' "$message" >&2
+ return 1
+}
+
+require_config_var() {
+ local -r name="$1"; shift
+
+ if [ -z "${!name+x}" ] || [ -z "${!name}" ]; then
+ config_error "$name must be set in shuriken configuration"
+ fi
+}
+
+validate_positive_integer_config_var() {
+ local -r name="$1"; shift
+ local -r value="${!name}"
+
+ if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
+ config_error "$name must be a positive integer"
+ fi
+}
+
+validate_optional_positive_integer_config_var() {
+ local -r name="$1"; shift
+
+ if [ -n "${!name}" ]; then
+ validate_positive_integer_config_var "$name"
+ fi
+}
+
+validate_yes_no_config_var() {
+ local -r name="$1"; shift
+ local -r value="${!name}"
+
+ case "$value" in
+ yes|no)
+ ;;
+ *)
+ config_error "$name must be yes or no"
+ ;;
+ esac
+}
+
+validate_dist_dir() {
+ local existing_parent
+
+ if [ -e "$DIST_DIR" ]; then
+ if [ ! -d "$DIST_DIR" ]; then
+ config_error "DIST_DIR $DIST_DIR must be a directory"
+ fi
+ if [[ ! -w "$DIST_DIR" || ! -x "$DIST_DIR" ]]; then
+ config_error "DIST_DIR $DIST_DIR must be writable"
+ fi
+
+ return
+ fi
+
+ existing_parent=$(existing_parent_dir "$DIST_DIR")
+
+ if [ ! -d "$existing_parent" ]; then
+ config_error "DIST_DIR parent $existing_parent must be a directory"
+ fi
+ if [[ ! -w "$existing_parent" || ! -x "$existing_parent" ]]; then
+ config_error "DIST_DIR parent $existing_parent must be writable"
+ fi
+}
+
+validate_template_dir_access() {
+ if [[ ! -d "$TEMPLATE_DIR" || ! -r "$TEMPLATE_DIR" \
+ || ! -x "$TEMPLATE_DIR" ]]; then
+ config_error "TEMPLATE_DIR $TEMPLATE_DIR must be a readable directory"
+ fi
+}
+
+validate_template_file() {
+ local -r template_name="$1"; shift
+
+ if [ ! -r "$TEMPLATE_DIR/$template_name.tmpl" ]; then
+ config_error \
+ "template file $TEMPLATE_DIR/$template_name.tmpl must be readable"
+ fi
+}
+
+validate_template_dir() {
+ local template_name
+ local -a required_templates=(
+ details
+ footer
+ header
+ next
+ prev
+ preview
+ redirect
+ view
+ )
+
+ validate_template_dir_access
+
+ if [ "${SPLASH_PAGE:-yes}" = yes ]; then
+ required_templates+=(splash)
+ fi
+
+ for template_name in "${required_templates[@]}"; do
+ valida