Files
Varaverk/Tools/container_data_export.sh
T
Gmer4Lfe 6623d1e776 Fix dead/incorrect vars and consolidate duplicated logic into common.sh
Codebase-wide audit pass: fixed real bugs (SSH hangs missing BatchMode,
local-outside-function no-ops, variable name collisions, a truncated
ratio calc, wrong state-dir path, DARK vs NO_INTERNET drift, and more),
then pulled logic that was duplicated across multiple scripts — arr
cleanup safety gates, docker restart ordering, container maintenance
stop/restart, watchdog state-file helpers, partnership role resolution,
cert expiry checks, remote node discovery, and TMDB discovery scoring —
into common.sh so each now has a single implementation.
2026-07-03 23:52:33 -04:00

247 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ==============================================================================================
# ============================= Container Data Export ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Exports a container's appdata directory to a compressed tar archive. Stops
# the container before archiving and restarts it after — ensures a clean,
# consistent backup. Use before major updates, pool migrations, destructive
# appdata operations, or when archiving a container being removed from the stack.
#
# Output: ContainerName_YYYY-MM-DD_HH-MM.tar.gz — timestamped, no overwrite.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Archive Verification Before Restart
# The archive is tested with tar --test-file before the container is restarted.
# A corrupt archive is not a usable backup — this catches tar failures, I/O
# errors, and truncated writes before declaring success. If verification fails,
# the container is still restarted (appdata is unchanged) and an error logged.
#
# Conservative Space Estimate
# Required space is estimated as appdata size × 1.1 (10% buffer). The actual
# compressed archive will typically be much smaller — database files compress
# well, media files do not. The estimate is a conservative floor, not a
# prediction.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Container Restart Rule
# Tracks whether the container was running before the export. Running containers
# are restarted after completion; already-stopped containers are left stopped.
# The restart happens on every exit path — a failed tar does not leave the
# container stuck stopped.
#
# Partial Archive Cleanup
# If tar fails, the incomplete archive is removed. A partial archive is worse
# than no archive — it can look valid but restore to an incomplete state.
#
# Docker Timeout
# DOCKER_TIMEOUT (default: 30s) caps all docker calls. Guards against a hung
# daemon blocking the script indefinitely.
#
# Notification Validated
# platform_require_cmd confirms the notify script is present before use.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# container_data_export.sh ContainerName /path/to/appdata /path/to/output/dir
# Stop container, create archive, verify, restart container.
# Example: container_data_export.sh Emby /mnt/media-servers/.../Emby /mnt/user/Backups/
#
# container_data_export.sh ContainerName /path/to/appdata /path/to/output/dir --dry-run
# Show what would be archived and estimated size. No container stop, no tar.
#
# container_data_export.sh ContainerName /path/to/appdata /path/to/output/dir --log
# Verbose output: space check, tar progress, verification result.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
DOCKER_TIMEOUT=30 # longer timeout — stop can take time on large containers
# ── Positional args ───────────────────────────────────────────────────────────────────────────
CONTAINER_NAME="${PARSED_ARGS[0]:-}"
APPDATA_PATH="${PARSED_ARGS[1]:-}"
OUTPUT_DIR="${PARSED_ARGS[2]:-}"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock
if ! command -v docker &>/dev/null; then
error "Docker command not found"
exit 1
fi
# detect_hosts() sets MY_ID — used in summary
detect_hosts
# Arg validation
if [[ -z "$CONTAINER_NAME" || -z "$APPDATA_PATH" || -z "$OUTPUT_DIR" ]]; then
error "Usage: container_data_export.sh <ContainerName> <appdata_path> <output_dir>"
error "Example: container_data_export.sh Emby /mnt/media-servers/Media_Server/Emby /mnt/user/Backups/"
exit 1
fi
if [[ ! -d "$APPDATA_PATH" ]]; then
error "Appdata path not found: $APPDATA_PATH"
exit 1
fi
if [[ ! -d "$OUTPUT_DIR" ]]; then
error "Output directory not found: $OUTPUT_DIR"
error "Create it first: mkdir -p \"$OUTPUT_DIR\""
exit 1
fi
# Space check — conservative: appdata × 1.1
APPDATA_SIZE_KB=$(du -sk "$APPDATA_PATH" 2>/dev/null | cut -f1)
OUTPUT_FREE_KB=$(df "$OUTPUT_DIR" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
REQUIRED_KB=$(( APPDATA_SIZE_KB * 11 / 10 ))
APPDATA_SIZE_H=$(du -sh "$APPDATA_PATH" 2>/dev/null | cut -f1)
OUTPUT_FREE_H=$(df -h "$OUTPUT_DIR" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
if [[ "$OUTPUT_FREE_KB" -lt "$REQUIRED_KB" ]]; then
error "Insufficient space in $OUTPUT_DIR"
error "Estimated need: ~${APPDATA_SIZE_H} (×1.1 conservative) — available: ${OUTPUT_FREE_H}"
exit 1
fi
log "Container: $CONTAINER_NAME"
log "Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
log "Output: $OUTPUT_DIR ($OUTPUT_FREE_H free)"
log "Space check passed"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ── Ensure container is restarted on any exit if it was running ────────────────────────────────
CONTAINER_WAS_RUNNING=false
ARCHIVE_PATH=""
cleanup_on_exit() {
local exit_code=$?
# Remove partial archive on failure
if [[ "$exit_code" -ne 0 && -n "$ARCHIVE_PATH" && -f "$ARCHIVE_PATH" ]]; then
warn "Removing partial archive: $ARCHIVE_PATH"
rm -f "$ARCHIVE_PATH" 2>/dev/null
fi
container_force_restart_if_needed "$CONTAINER_NAME" "$CONTAINER_WAS_RUNNING"
}
trap cleanup_on_exit EXIT
# ==============================================================================================
# ━━━ Stop Container ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_STOP Stop Container ━━━"
container_stop_for_maintenance "$CONTAINER_NAME" CONTAINER_WAS_RUNNING \
"Stopping $CONTAINER_NAME for clean export..." log || exit 1
[[ "$CONTAINER_WAS_RUNNING" == false ]] && \
echo "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"
# ==============================================================================================
# ━━━ Archive ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Archive ━━━"
TIMESTAMP=$(date '+%Y-%m-%d_%H-%M')
ARCHIVE_NAME="${CONTAINER_NAME}_${TIMESTAMP}.tar.gz"
ARCHIVE_PATH="${OUTPUT_DIR}/${ARCHIVE_NAME}"
warn "Creating: $ARCHIVE_PATH"
warn "Source: $APPDATA_PATH ($APPDATA_SIZE_H)"
START=$(date +%s)
ARCHIVE_VERIFIED=false
if [[ "$DRY_RUN" == false ]]; then
if tar -czf "$ARCHIVE_PATH" \
-C "$(dirname "$APPDATA_PATH")" \
"$(basename "$APPDATA_PATH")" 2>/dev/null; then
ARCHIVE_SIZE=$(du -sh "$ARCHIVE_PATH" 2>/dev/null | cut -f1)
warn "Archive created: $ARCHIVE_NAME ($ARCHIVE_SIZE)"
# Verify archive integrity before declaring success
log "Verifying archive..."
if tar --test-label -f "$ARCHIVE_PATH" 2>/dev/null || \
tar -tzf "$ARCHIVE_PATH" >/dev/null 2>&1; then
echo "Archive verified ✅"
ARCHIVE_VERIFIED=true
else
error "Archive verification FAILED — archive may be corrupt"
error "Container will be restarted but DO NOT rely on this backup"
notify "Container export archive corrupt — $CONTAINER_NAME backup may be unusable" \
"Container Export" "warning"
fi
else
error "tar failed — archive creation unsuccessful"
exit 1
fi
else
warn "DRY RUN — would create: $ARCHIVE_PATH"
ARCHIVE_VERIFIED=true
fi
END=$(date +%s)
# ==============================================================================================
# ━━━ Restart Container ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_START Restart Container ━━━"
container_restart_after_maintenance "$CONTAINER_NAME" "$CONTAINER_WAS_RUNNING" 3 "Container Export"
# Clear trap — clean exit, cleanup_on_exit no longer needed
trap - EXIT
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY CONTAINER EXPORT SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_CONTAINERS Container: $CONTAINER_NAME"
echo "$ICON_DISK Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
echo "$ICON_SYNC Archive: ${ARCHIVE_NAME:-DRY RUN} ${ARCHIVE_SIZE:+($ARCHIVE_SIZE)}"
echo "$ICON_SHIELD Verified: $([[ "$ARCHIVE_VERIFIED" == true ]] && echo "✅" || echo "❌ FAILED")"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no changes made"
elif [[ "$ARCHIVE_VERIFIED" == true && "$RESTART_OK" == true ]]; then
echo "$ICON_DONE Status: done — $ARCHIVE_NAME"
elif [[ "$ARCHIVE_VERIFIED" == false ]]; then
echo "$ICON_ERROR Status: archive verification FAILED — check backup before relying on it"
else
warn "Status: complete with warnings — check restart status above"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ARCHIVE_VERIFIED" == false ]] && exit 1
exit 0