#!/usr/bin/env bash # Called only under the shared lock; --check never invokes this maintenance. clean_backup_scratch() { python3 "$LIB_DIR/space.py" clean "$WORKDIR" "$STAGING_ROOT" "$ARCHIVE_DIR" "$ARCHIVE_PREFIX" "$RESTORE_ROOT" } ensure_space() { local path=$1 additional=${2:-0} avail name dir candidates [[ $additional =~ ^(0|[1-9][0-9]{0,15})$ ]] || die 'Invalid or excessive space request' local required=$((MIN_FREE_GB * 1024 * 1024 * 1024 + additional)) avail=$(df -PB1 -- "$path" | awk 'END{print $4}') (( avail < required )) || return 0 echo "-- Space recovery: need $required bytes, available $avail on $path" if [[ $ENABLE_UPLOAD == true && $(stat -c %d "$path") == "$(stat -c %d "$ARCHIVE_DIR")" ]]; then check_remote candidates=$(python3 "$LIB_DIR/space.py" eligible "$ARCHIVE_DIR" "$ARCHIVE_PREFIX" "$RETENTION_KEEP_LAST") while IFS= read -r name; do [[ -n $name ]] || continue dir="$ARCHIVE_DIR/$name" # A manually selected local restore source is never a cleanup candidate. [[ -z ${LOCAL_RUN:-} || $(realpath -m "$LOCAL_RUN") != "$dir" ]] || continue echo "-- Space recovery: verifying offsite copy before local removal: $name" if helper verify "$dir" >/dev/null && rc check "$dir" "${RCLONE_REMOTE_BASE%/}/$name" --download --exclude /UPLOADED; then python3 "$LIB_DIR/space.py" remove "$dir" echo "-- Space recovery: removed local copy $name; verified offsite copy retained" else echo "WARNING: Keeping $name: local/offsite verification failed" fi avail=$(df -PB1 -- "$path" | awk 'END{print $4}') (( avail < required )) || return 0 done <<< "$candidates" fi die "Insufficient space after safe cleanup on $path: need $required bytes, available $avail; protected backups/recovery data retained" } prepare_backup_space() { if [[ -f $WORKDIR/restore-test/.owner ]]; then local cleanup_script="$LIB_DIR/../usr-local-sbin/app-restore-test.sh" [[ -f $cleanup_script ]] || cleanup_script=/usr/local/sbin/app-restore-test.sh APP_BACKUP_INHERIT_LOCK=1 bash "$cleanup_script" --config "$CONFIG_FILE" --cleanup fi clean_backup_scratch local i stats bytes count total=0 db_size # Dry-run obeys the exact copy exclusions. No dump, copy or service stop. for i in "${!LABELS[@]}"; do check_source_path "${SOURCES[i]}" exclusions "${LABELS[i]}" stats=$(rsync -aHAX --dry-run --stats --numeric-ids "${EXCLUDES[@]}" -- "${SOURCES[i]}/" "$STAGING_ROOT/.space-estimate/" 8>&- 9>&-) bytes=$(awk '/^Total file size:/ {gsub(/,/, "", $4); print $4}' <<< "$stats") count=$(awk '/^Number of files:/ {gsub(/,/, "", $4); print $4}' <<< "$stats") [[ $bytes =~ ^[0-9]+$ && $count =~ ^[0-9]+$ ]] || die 'Cannot estimate source allocation' total=$((total + bytes + count * 8192)) done for i in "${!DB_NAMES[@]}"; do secure_file "${DB_CNFS[i]}" db_size=$("$DB_CLIENT_BIN" --defaults-extra-file="${DB_CNFS[i]}" --database="${DB_NAMES[i]}" --batch --skip-column-names --execute='SELECT COALESCE(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema=DATABASE()') [[ $db_size =~ ^[0-9]+$ ]] || die 'Cannot estimate database size' total=$((total + db_size * 3 + 67108864)) done # Account for SQL expansion, tar metadata and growth between inventory/copy. total=$((total * 12 / 10 + 67108864)) echo "-- Estimated snapshot budget: $total bytes (+ archive overlap and free-space reserve)" if [[ $(stat -c %d "$STAGING_ROOT") == "$(stat -c %d "$ARCHIVE_DIR")" ]]; then ensure_space "$STAGING_ROOT" "$((total * 2))" else ensure_space "$STAGING_ROOT" "$total" ensure_space "$ARCHIVE_DIR" "$total" fi }