diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-11 22:31:55 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-11 22:31:55 +0300 |
| commit | 44d3f7a689751633f8ac5f06b4ba0be8c056bb42 (patch) | |
| tree | 8957632b4e4af3fec8724858f6196392dd06a05d /src/lib | |
| parent | e6c32dd8d134b0f90a144fd415585c0b12ce0d74 (diff) | |
Split config module concerns for task 9m0
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/config.cli.source.sh | 109 | ||||
| -rw-r--r-- | src/lib/config.print.source.sh | 46 | ||||
| -rw-r--r-- | src/lib/config.source.sh | 629 | ||||
| -rw-r--r-- | src/lib/config.staging.source.sh | 216 | ||||
| -rw-r--r-- | src/lib/config.sync.source.sh | 16 | ||||
| -rw-r--r-- | src/lib/config.validate.source.sh | 237 |
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 |
