595 lines
18 KiB
Bash
Executable File
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 "$@"
|