#!/bin/bash # Paperless Restore Script # Location: /home/citadel/services/paperless/restore # Usage: ./restore --test|--production|--extract # ./restore --clean # ./restore --clean-all set -euo pipefail # Load template configuration source "$(dirname "$(dirname "$(dirname "$0")")")/backup/service-script-template.sh" # Configuration readonly TEST_DIR="$SERVICE_DIR/test-restore" 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 --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 "$@"