2025-06-24 19:44:41 +02:00

595 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Paperless Restore Script
# Location: /home/citadel/services/paperless/restore
# Usage: ./restore <snapshot_id> --test|--production|--extract
# ./restore --clean
# ./restore --clean-all
set -euo pipefail
# Configuration
readonly SERVICE_NAME="paperless"
readonly SERVICE_DIR="/home/citadel/services/$SERVICE_NAME"
readonly TEST_DIR="$SERVICE_DIR/test-restore"
readonly CONFIG_FILE="/home/citadel/backup/restic.conf"
readonly SCRIPT_NAME=$(basename "$0")
# Colors
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'
# Logging functions
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
# Global variables
SNAPSHOT_ID=""
MODE=""
TEMP_DIR=""
# Show help
show_help() {
cat << EOF
Usage: $SCRIPT_NAME <snapshot_id> --test|--production|--extract
$SCRIPT_NAME --clean|--clean-all
Arguments:
snapshot_id The restic snapshot ID to restore from
Options:
--test Restore to test instance (isolated environment)
--production Restore to production instance (NOT YET IMPLEMENTED)
--extract Extract snapshot to /tmp directory only
--clean Clean test environment (stop containers and remove volumes)
--clean-all Clean test environment AND remove all temp directories
-h, --help Show this help message
Examples:
$SCRIPT_NAME abc123 --test # Restore snapshot abc123 to test instance
$SCRIPT_NAME abc123 --extract # Extract snapshot abc123 to /tmp only
$SCRIPT_NAME latest --production # Restore latest snapshot to production
$SCRIPT_NAME --clean # Clean test environment
$SCRIPT_NAME --clean-all # Clean test environment + temp directories
The test instance will be created in: $TEST_DIR
EOF
}
# Cleanup function
cleanup() {
if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ] && [[ "$TEMP_DIR" == /tmp/* ]]; then
log "Cleaning up temporary directory: $TEMP_DIR"
rm -rf "$TEMP_DIR"
fi
}
# Set up cleanup on exit (only for extract mode)
setup_cleanup() {
if [ "$MODE" = "extract" ]; then
trap cleanup EXIT
fi
}
# Parse arguments
parse_arguments() {
if [ $# -eq 0 ]; then
show_help
exit 0
fi
while [ $# -gt 0 ]; do
case "$1" in
--test)
MODE="test"
shift
;;
--production)
MODE="production"
shift
;;
--extract)
MODE="extract"
shift
;;
--clean)
MODE="clean"
shift
;;
--clean-all)
MODE="clean-all"
shift
;;
-h|--help)
show_help
exit 0
;;
-*)
error "Unknown option: $1"
;;
*)
if [ -z "$SNAPSHOT_ID" ]; then
SNAPSHOT_ID="$1"
else
error "Too many arguments"
fi
shift
;;
esac
done
# Validate required arguments
if [ "$MODE" != "clean" ] && [ "$MODE" != "clean-all" ]; then
if [ -z "$SNAPSHOT_ID" ]; then
error "Snapshot ID is required (except for --clean or --clean-all mode)"
fi
fi
if [ -z "$MODE" ]; then
error "Mode (--test, --production, --extract, --clean, or --clean-all) is required"
fi
# Clean modes don't need snapshot ID
if [ "$MODE" = "clean" ] || [ "$MODE" = "clean-all" ]; then
if [ -n "$SNAPSHOT_ID" ]; then
error "Snapshot ID should not be provided with --clean or --clean-all mode"
fi
fi
}
# Check prerequisites
check_prerequisites() {
# Skip restic checks for clean modes
if [ "$MODE" = "clean" ] || [ "$MODE" = "clean-all" ]; then
# Only check Docker for clean modes
if ! command -v docker &>/dev/null; then
error "Docker not found"
fi
if ! docker compose version &>/dev/null; then
error "Docker Compose not found"
fi
log "Prerequisites OK (clean mode)"
return
fi
# Check configuration file
if [ ! -f "$CONFIG_FILE" ]; then
error "Config file not found: $CONFIG_FILE"
fi
# Source config
source "$CONFIG_FILE"
# Check required variables
if [ -z "${RESTIC_REPOSITORY:-}" ]; then
error "RESTIC_REPOSITORY not defined in config"
fi
# Test restic access
if ! restic snapshots &>/dev/null; then
error "Cannot access Restic repository"
fi
# Check Docker (except for extract mode)
if [ "$MODE" != "extract" ]; then
if ! command -v docker &>/dev/null; then
error "Docker not found"
fi
# Check docker compose
if ! docker compose version &>/dev/null; then
error "Docker Compose not found"
fi
fi
log "Prerequisites OK"
}
# Create secure temporary directory
create_temp_dir() {
if [ "$MODE" = "extract" ]; then
# For extract mode, create in /tmp with readable name
TEMP_DIR="/tmp/paperless-extract-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$TEMP_DIR"
chmod 755 "$TEMP_DIR" # More permissive for extract mode
else
# For other modes, use secure temp
TEMP_DIR=$(mktemp -d "/tmp/paperless-restore-$(date +%Y%m%d-%H%M%S)-XXXXXX")
chmod 700 "$TEMP_DIR"
fi
if [ ! -d "$TEMP_DIR" ]; then
error "Failed to create temporary directory"
fi
log "Created temporary directory: $TEMP_DIR"
}
# Extract snapshot
extract_snapshot() {
log "Extracting snapshot $SNAPSHOT_ID to temporary directory"
if ! restic restore "$SNAPSHOT_ID" --target "$TEMP_DIR"; then
error "Failed to extract snapshot $SNAPSHOT_ID"
fi
success "Snapshot extracted successfully"
# Show what was extracted
log "Extracted contents:"
ls -la "$TEMP_DIR" | head -10
# Show paperless data structure if exists
if [ -d "$TEMP_DIR/tmp/paperless" ]; then
echo ""
log "Paperless data structure:"
ls -la "$TEMP_DIR/tmp/paperless/" | head -10
fi
}
# Extract only mode
extract_only() {
log "Starting extract-only mode"
# Create temp directory in /tmp
create_temp_dir
# Extract snapshot
extract_snapshot
# Display information about extracted content
echo ""
success "Snapshot $SNAPSHOT_ID extracted to: $TEMP_DIR"
echo ""
echo "📁 Extracted structure:"
if [ -d "$TEMP_DIR/tmp/paperless" ]; then
echo " Main data location: $TEMP_DIR/tmp/paperless/"
ls -la "$TEMP_DIR/tmp/paperless/" | head -10
else
find "$TEMP_DIR" -maxdepth 2 -type d | head -20
fi
echo ""
echo "📊 Content summary:"
echo " Database dumps: $(find "$TEMP_DIR" -name "*.sql" | wc -l) files"
echo " Data directories: $(find "$TEMP_DIR/tmp/paperless" -maxdepth 1 -type d 2>/dev/null | grep -v "^$TEMP_DIR/tmp/paperless$" | wc -l) directories"
echo " Total size: $(du -sh "$TEMP_DIR" | cut -f1)"
echo ""
echo "💡 Manual inspection commands:"
echo " cd $TEMP_DIR/tmp/paperless"
echo " ls -la"
echo " find . -name '*.sql' -exec ls -lh {} +"
echo ""
echo "⚠️ The directory will remain until you delete it manually:"
echo " rm -rf $TEMP_DIR"
echo ""
# Don't run cleanup for extract mode - leave directory for user
trap - EXIT
}
# Clean test environment
clean_test_environment() {
log "Starting cleanup of test environment"
# Navigate to service directory to ensure we have access to .env
cd "$SERVICE_DIR"
# Stop test containers
log "Stopping test containers..."
if [ -d "$TEST_DIR" ]; then
cd "$TEST_DIR"
docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down --remove-orphans 2>/dev/null || true
cd "$SERVICE_DIR"
else
log "Test directory doesn't exist, checking for running containers anyway..."
fi
# Try to stop containers by name pattern (in case they're running from different location)
log "Stopping any remaining paperless test containers..."
docker stop paperless-webserver-test paperless-db-test paperless-broker-test paperless-gotenberg-test paperless-tika-test 2>/dev/null || true
docker rm paperless-webserver-test paperless-db-test paperless-broker-test paperless-gotenberg-test paperless-tika-test 2>/dev/null || true
# Remove test volumes
log "Removing test volumes..."
local volumes_to_remove=(
"paperless-ngx_pgdata-test"
"redisdata-test"
"paperless-data-test"
"paperless-media-test"
"paperless-export-test"
"paperless-consume-test"
)
for volume in "${volumes_to_remove[@]}"; do
if docker volume ls -q | grep -q "^${volume}$"; then
log "Removing volume: $volume"
docker volume rm "$volume" 2>/dev/null || warning "Failed to remove volume: $volume"
else
log "Volume not found (already removed): $volume"
fi
done
# Clean up any dangling images related to paperless
log "Cleaning up dangling images..."
docker image prune -f &>/dev/null || true
success "Test environment cleaned successfully"
echo ""
echo "🧹 Cleanup completed!"
echo " ✅ Test containers stopped and removed"
echo " ✅ Test volumes removed"
echo " ✅ Dangling images cleaned"
echo ""
echo "💡 The production environment remains untouched."
echo "💡 The test directory ($TEST_DIR) is preserved."
echo "💡 You can now run a fresh test restore if needed."
}
# Clean all environment (test + temp directories)
clean_all_environment() {
log "Starting cleanup of test environment and temp directories"
# First, run the standard test environment cleanup
clean_test_environment
# Then clean temporary directories
echo ""
log "Cleaning temporary directories..."
# Find and remove paperless restore temp directories
local temp_dirs_restore=()
local temp_dirs_extract=()
# Use find to safely locate temp directories
while IFS= read -r -d '' dir; do
temp_dirs_restore+=("$dir")
done < <(find /tmp -maxdepth 1 -type d -name "paperless-restore-*" -print0 2>/dev/null)
while IFS= read -r -d '' dir; do
temp_dirs_extract+=("$dir")
done < <(find /tmp -maxdepth 1 -type d -name "paperless-extract-*" -print0 2>/dev/null)
# Remove restore temp directories
if [ ${#temp_dirs_restore[@]} -gt 0 ]; then
log "Found ${#temp_dirs_restore[@]} restore temp directories to remove"
for dir in "${temp_dirs_restore[@]}"; do
if [ -d "$dir" ]; then
log "Removing: $(basename "$dir")"
rm -rf "$dir" || warning "Failed to remove: $dir"
fi
done
else
log "No restore temp directories found"
fi
# Remove extract temp directories
if [ ${#temp_dirs_extract[@]} -gt 0 ]; then
log "Found ${#temp_dirs_extract[@]} extract temp directories to remove"
for dir in "${temp_dirs_extract[@]}"; do
if [ -d "$dir" ]; then
log "Removing: $(basename "$dir")"
rm -rf "$dir" || warning "Failed to remove: $dir"
fi
done
else
log "No extract temp directories found"
fi
# Show final summary
echo ""
success "Complete cleanup finished!"
echo ""
echo "🧹 Full cleanup completed!"
echo " ✅ Test containers stopped and removed"
echo " ✅ Test volumes removed"
echo " ✅ Dangling images cleaned"
echo " ✅ Restore temp directories removed: ${#temp_dirs_restore[@]}"
echo " ✅ Extract temp directories removed: ${#temp_dirs_extract[@]}"
echo ""
echo "💡 The production environment remains untouched."
echo "💡 The test directory ($TEST_DIR) is preserved."
echo "💡 System is now clean and ready for fresh operations."
}
# Restore to test instance
restore_to_test() {
log "Starting restore to test instance"
# Create test directory if it doesn't exist
mkdir -p "$TEST_DIR"
# Navigate to test directory
cd "$TEST_DIR"
log "Stopping test containers (if running)..."
docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down --remove-orphans 2>/dev/null || true
# Remove existing test volumes to ensure clean restore
log "Removing existing test volumes..."
docker volume rm paperless-ngx_pgdata-test redisdata-test \
paperless-data-test paperless-media-test \
paperless-export-test paperless-consume-test 2>/dev/null || true
# Find database dump
local db_dump
db_dump=$(find "$TEMP_DIR" -name "*_db_*.sql" | head -1)
if [ -z "$db_dump" ]; then
error "No database dump found in snapshot"
fi
log "Found database dump: $(basename "$db_dump")"
# Create PostgreSQL volume before starting database container
log "Creating PostgreSQL volume..."
docker volume create paperless-ngx_pgdata-test
# Start only the database container for restore
log "Starting test database container..."
docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test up -d db-test
# Wait for database to be ready
log "Waiting for database to be ready..."
sleep 15
# Source environment variables
if [ -f "$SERVICE_DIR/.env" ]; then
source "$SERVICE_DIR/.env"
else
error "Environment file not found: $SERVICE_DIR/.env"
fi
# Restore database
log "Restoring database from dump..."
if docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test exec -T db-test psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$db_dump"; then
success "Database restored successfully"
else
error "Database restore failed"
fi
# Stop database container
docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test down
# Restore data volumes using temporary containers
log "Restoring data volumes..."
# Create ALL volumes (Docker Compose will not create them since they're external)
log "Creating all required volumes..."
docker volume create paperless-ngx_pgdata-test
docker volume create redisdata-test
docker volume create paperless-data-test
docker volume create paperless-media-test
docker volume create paperless-export-test
docker volume create paperless-consume-test
# Function to restore volume data
restore_volume_data() {
local source_path="$1"
local volume_name="$2"
local container_path="$3"
if [ -d "$source_path" ]; then
log "Restoring $volume_name from $source_path"
docker run --rm \
-v "$volume_name:$container_path" \
-v "$source_path:$container_path-source:ro" \
alpine:latest \
sh -c "cp -rf $container_path-source/* $container_path/ 2>/dev/null || true"
else
warning "Source path not found: $source_path"
fi
}
# Restore each data directory
restore_volume_data "$TEMP_DIR/tmp/paperless/data" "paperless-data-test" "/data"
restore_volume_data "$TEMP_DIR/tmp/paperless/media" "paperless-media-test" "/media"
restore_volume_data "$TEMP_DIR/tmp/paperless/export" "paperless-export-test" "/export"
restore_volume_data "$TEMP_DIR/tmp/paperless/consume" "paperless-consume-test" "/consume"
# Start all test containers
log "Starting test instance..."
docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test up -d
# Wait for services to be ready
log "Waiting for services to start..."
sleep 30
# Check if webserver is running
if docker compose --env-file "$SERVICE_DIR/.env" -p paperless-test ps --filter "status=running" | grep -q "webserver-test"; then
success "Test instance started successfully"
echo ""
echo "🎉 Test instance is ready!"
echo "📍 Container name: paperless-webserver-test"
echo "🌐 Access: Configure SWAG to redirect paperless.alouettes.jombi.fr temporarily"
echo "🔧 Docker compose location: $TEST_DIR"
echo ""
echo "💡 To stop the test instance:"
echo " cd $TEST_DIR && docker compose --env-file $SERVICE_DIR/.env -p paperless-test down"
echo ""
echo "💡 To clean test environment completely:"
echo " $SERVICE_DIR/restore --clean"
else
error "Test instance failed to start properly"
fi
}
# Restore to production (not implemented)
restore_to_production() {
echo ""
echo "🚧 PRODUCTION RESTORE NOT YET IMPLEMENTED 🚧"
echo ""
echo "Production restore functionality is not yet available."
echo "This feature requires additional safety measures and validation."
echo ""
echo "For now, you can:"
echo "1. Use --test mode to restore to the test instance"
echo "2. Verify the restored data in the test environment"
echo "3. Manually copy data from test to production if needed"
echo ""
echo "Production restore will be implemented in a future version."
echo ""
exit 1
}
# Main function
main() {
echo "=== Paperless Restore Script ==="
# Parse arguments
parse_arguments "$@"
# Display operation info
if [ "$MODE" = "clean" ]; then
log "Mode: $MODE"
else
log "Snapshot ID: $SNAPSHOT_ID"
log "Mode: $MODE"
fi
# Check prerequisites
check_prerequisites
# Setup cleanup if needed
setup_cleanup
# Execute based on mode
case "$MODE" in
clean)
clean_test_environment
;;
clean-all)
clean_all_environment
;;
extract)
extract_only
;;
test)
# Create temporary directory and extract
create_temp_dir
extract_snapshot
restore_to_test
;;
production)
# Create temporary directory and extract
create_temp_dir
extract_snapshot
restore_to_production
;;
*)
error "Invalid mode: $MODE"
;;
esac
success "Operation completed!"
}
# Entry point
main "$@"