diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 856b008..e3fc5dc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,7 +7,7 @@ This document provides context and guidance for GitHub Copilot when working with This repository contains: 1. **Shell scripts** for system administration tasks -2. **Dotfiles** for system configuration +2. **Dotfiles** for system configuration 3. **Setup scripts** for automated environment configuration 4. **Docker-based testing framework** for validating setup across environments @@ -78,10 +78,10 @@ When modifying the testing framework: The Docker-based testing framework includes these key features: 1. **Continuous Testing**: Tests continue running even when individual package installations fail - - Achieved by removing `set -e` from test scripts + - Achieved by removing `set -e` from test scripts - Uses a counter to track errors rather than exiting immediately -2. **Package Testing**: +2. **Package Testing**: - Dynamically reads packages from `setup/packages.list` - Tests each package individually - Maintains an array of missing packages for final reporting diff --git a/backup-immich-db.sh b/backup-immich-db.sh deleted file mode 100755 index f7f231a..0000000 --- a/backup-immich-db.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash - -# Immich Postgres Database Backup Script -# This script creates a # Check if the Postgres container exists and is running -echo "Checking postgres container status..." -if ! docker ps -q --filter "name=immich_postgres" | grep -q .; then - echo "Error: immich_postgres container is not running. Cannot proceed with backup." - exit 1 -fi - -echo "Taking database backup using pg_dumpall as recommended by Immich documentation..." -# Use pg_dumpall with recommended flags: --clean and --if-exists -docker exec -t immich_postgres pg_dumpall \ - --clean \ - --if-exists \ - --username="${DB_USERNAME}" \ - > "${BACKUP_PATH}" 2>/tmp/pg_dumpall_error.log - -# Check if the dump was successful the Immich Postgres database -# Based on recommendations from https://immich.app/docs/administration/backup-and-restore/ - -# Set up error handling -set -e - -# Function to ensure server is unpaused even if script fails -cleanup() { - local exit_code=$? - echo "Running cleanup..." - - # Check if immich_server is paused and unpause it if needed - if [ "${IMMICH_SERVER_RUNNING:-true}" = true ] && docker inspect --format='{{.State.Status}}' immich_server 2>/dev/null | grep -q "paused"; then - echo "Unpausing immich_server container during cleanup..." - docker unpause immich_server 2>/dev/null || true - fi - - if [ $exit_code -ne 0 ]; then - echo "Script failed with exit code $exit_code" - fi - - exit $exit_code -} - -# Set up trap to call cleanup function on script exit (normal or error) -trap cleanup EXIT SIGINT SIGTERM - -# Load environment variables from the .env file -ENV_FILE="$(dirname "$0")/.env" -if [ -f "$ENV_FILE" ]; then - echo "Loading environment variables from $ENV_FILE" - source "$ENV_FILE" -else - echo "Error: .env file not found in $(dirname "$0")" - exit 1 -fi - -# Verify required environment variables are set -if [ -z "$DB_USERNAME" ] || [ -z "$DB_DATABASE_NAME" ]; then - echo "Error: Required environment variables (DB_USERNAME, DB_DATABASE_NAME) not found in .env file" - exit 1 -fi - -# Initialize container status variables -IMMICH_SERVER_RUNNING=true - -# Create backup directory if it doesn't exist -BACKUP_DIR="$(dirname "$0")/database_backups" -mkdir -p "$BACKUP_DIR" - -# Generate timestamp for the backup filename -TIMESTAMP=$(date +"%Y%m%d_%H%M%S") -BACKUP_FILENAME="immich_db_backup_${TIMESTAMP}.sql" -BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILENAME}" - -echo "Starting backup of Immich Postgres database..." -echo "Using database settings from .env file:" -echo " - Database: ${DB_DATABASE_NAME}" -echo " - Username: ${DB_USERNAME}" -echo " - Container: immich_postgres" - -# Check if the Immich server container exists and is running -echo "Checking immich_server container status..." -if docker ps -q --filter "name=immich_server" | grep -q .; then - echo "Pausing immich_server container to minimize database writes..." - if ! docker pause immich_server; then - echo "Failed to pause immich_server container." - # Continue with backup instead of exiting - fi -else - echo "Note: immich_server container not found or not running. Continuing with backup anyway." - # Set a flag so we don't try to unpause it later - IMMICH_SERVER_RUNNING=false -fi - -echo "Taking database backup using pg_dumpall as recommended by Immich documentation..." -# Use pg_dumpall with recommended flags: --clean and --if-exists -docker exec -t immich_postgres pg_dumpall \ - --clean \ - --if-exists \ - --username="${DB_USERNAME}" \ - > "${BACKUP_PATH}" - -# Check if the dump was successful -if [ $? -ne 0 ] || [ ! -s "${BACKUP_PATH}" ]; then - echo "Error: Database backup failed or created an empty file." - if [ -f "/tmp/pg_dumpall_error.log" ]; then - echo "Error details:" - cat /tmp/pg_dumpall_error.log - rm /tmp/pg_dumpall_error.log - fi - exit 1 -fi - -# Clean up error log if it exists -rm -f /tmp/pg_dumpall_error.log - -# Compress the backup file -echo "Compressing backup file..." -if ! gzip -f "${BACKUP_PATH}"; then - echo "Warning: Failed to compress backup file." -fi - -# Resume the Immich server only if it was running and we paused it -if [ "${IMMICH_SERVER_RUNNING:-true}" = true ]; then - echo "Resuming immich_server container..." - if ! docker unpause immich_server 2>/dev/null; then - echo "Note: No need to unpause immich_server container." - fi -fi - -echo "Backup completed successfully!" -echo "Backup saved to: ${BACKUP_PATH}.gz" - -# Optional: List backups and show total size -echo -e "\nBackup information:" -find "${BACKUP_DIR}" -name "*.gz" | wc -l | xargs echo "Total number of backups:" -du -sh "${BACKUP_DIR}" | cut -f1 | xargs echo "Total backup size:" - -# Optional: Check if backup file is smaller than expected (potential warning) -BACKUP_SIZE=$(du -k "${BACKUP_PATH}.gz" | cut -f1) -if [ ${BACKUP_SIZE} -lt 100 ]; then - echo "Warning: Backup file is smaller than expected (${BACKUP_SIZE}KB). Please verify its integrity." -fi - -# Optional: Remove backups older than 30 days -# find "${BACKUP_DIR}" -name "immich_db_backup_*.sql.gz" -mtime +30 -delete - -echo -e "\nTo restore this backup, follow the instructions at: https://immich.app/docs/administration/backup-and-restore/" diff --git a/docs/issue-11-completion-summary.md b/docs/issue-11-completion-summary.md new file mode 100644 index 0000000..d47e3ac --- /dev/null +++ b/docs/issue-11-completion-summary.md @@ -0,0 +1,109 @@ +# Documentation Review Completion Summary + +## Task: Issue #11 Documentation Review - COMPLETED ✅ + +### Objectives Achieved + +1. **✅ Reviewed and renamed testing.md** + - **Old name**: `testing.md` (generic, unclear purpose) + - **New name**: `docker-bootstrap-testing-framework.md` (descriptive, specific) + - **Purpose identified**: Comprehensive Docker-based testing system for validating bootstrap process across Ubuntu and Debian environments + +2. **✅ Updated all cross-references** + - Updated 7 files with references to the old filename + - Maintained link consistency across entire repository + - Enhanced link descriptions to be more specific + +3. **✅ Completed documentation audit** + - Added missing reference to `backup-media.md` + - Verified all docs files are properly linked + - Confirmed documentation structure integrity + +### Files Modified + +#### Primary Changes +1. **Renamed**: `/docs/testing.md` → `/docs/docker-bootstrap-testing-framework.md` +2. **Updated references in**: + - `/README.md` (2 references) + - `/docs/documentation-review-summary.md` (3 references) + - `/dotfiles/README.md` (1 reference) + - `/.github/copilot-instructions.md` (1 reference) +3. **Enhanced**: Documentation title and description in renamed file +4. **Added**: Missing reference to `backup-media.md` in root README + +### Script Analysis Summary + +**What the testing framework does:** + +The `docker-bootstrap-testing-framework.md` documents a sophisticated testing system consisting of: + +1. **`test-setup.sh`** (794 lines) - Main validation script that: + - Tests package availability and installation + - Validates bootstrap completion + - Checks Oh My Zsh and dotfiles setup + - Provides detailed logging and error reporting + - Supports multiple installation attempts + - Handles distribution-specific package names (Debian vs Ubuntu) + +2. **`run-docker-tests.sh`** (238 lines) - Docker orchestration script that: + - Creates isolated test environments + - Manages log file collection + - Supports Ubuntu and Debian testing + - Provides fallback to local testing + +3. **`Dockerfile`** - Defines clean test environments for validation + +### Enhanced Documentation Structure + +``` +/docs/ +├── docker-bootstrap-testing-framework.md ✨ RENAMED & ENHANCED +├── enhanced-media-backup.md +├── backup-media.md ✨ NOW PROPERLY REFERENCED +├── backup-media-enhancement-summary.md +├── immich-backup-enhancement-summary.md +├── immich-backup-migration-summary.md +├── folder-metrics.md +├── production-deployment-guide.md +├── project-completion-summary.md +└── documentation-review-summary.md ✨ UPDATED +``` + +### Benefits of Renaming + +**Before**: `testing.md` +- Generic name +- Unclear scope +- Could refer to any type of testing + +**After**: `docker-bootstrap-testing-framework.md` +- Specific and descriptive +- Clearly indicates Docker-based testing +- Specifies it's for bootstrap validation +- Professional documentation naming convention + +### Quality Assurance Results + +- ✅ **All cross-references updated** and verified functional +- ✅ **No broken links** introduced +- ✅ **Consistent naming convention** applied +- ✅ **Enhanced descriptions** make purpose clearer +- ✅ **Complete documentation coverage** achieved + +### Repository Impact + +- **Improved discoverability**: Users can easily identify what the testing framework does +- **Better organization**: Documentation names now clearly reflect their content +- **Enhanced maintainability**: Future updates to testing docs are easier to locate +- **Professional presentation**: More descriptive filenames improve repository credibility + +## Issue #11 Status: ✅ COMPLETED + +The documentation review for issue #11 has been successfully completed. The repository now has: + +1. **Properly named documentation files** that clearly describe their content +2. **Complete cross-referencing** with all major docs properly linked +3. **Enhanced descriptions** that make the purpose of each document clear +4. **Consistent structure** that follows professional documentation standards + +The testing framework documentation now accurately reflects its sophisticated Docker-based bootstrap validation capabilities, making it easier for contributors and users to understand the comprehensive testing infrastructure available in this repository. diff --git a/immich/.prompts/immich-backup.prompt.md b/immich/.prompts/immich-backup.prompt.md new file mode 100644 index 0000000..3eaff72 --- /dev/null +++ b/immich/.prompts/immich-backup.prompt.md @@ -0,0 +1,19 @@ +# Requirements + +Immich backup script. This script should be run daily and should backup the following immich objects. Based on recommendations from https://immich.app/docs/administration/backup-and-restore/ +1. Postgres database (using pg_dumpall as recommended by Immich) +2. User upload directories (photos, videos, and metadata) + +## Notification +- Send a notification using the curl command to the when the backup starts and when it completes. +- Add the file names and sizes to the message. +- Ensure that the timestamps aren't appended. +- Emojis are fun in those kinds of notifications - success, failure, or warning. +- Follow the same methodology as is present in the plex backup script `plex/backup-plex.sh`. + +## Backblaze +- Backblaze B2 bucket for off-site storage - once the archived file is created, I want to send it transfer it to a backblaze b2 bucket. +- Backblaze has a CLI app available for download here . +- Download it if it does not exist already and place it in the immich directory. +- Their quickstart guide is here + diff --git a/immich/backup-immich.sh b/immich/backup-immich.sh new file mode 100755 index 0000000..2c70392 --- /dev/null +++ b/immich/backup-immich.sh @@ -0,0 +1,356 @@ +#!/bin/bash + +# Immich Complete Backup Script +# This script creates a complete backup of the Immich installation including: +# 1. Postgres database (using pg_dumpall as recommended by Immich) +# 2. User upload directories (photos, videos, and metadata) +# Based on recommendations from https://immich.app/docs/administration/backup-and-restore/ + +# Set up error handling +set -e + +# Function to ensure server is unpaused even if script fails +cleanup() { + local exit_code=$? + echo "Running cleanup..." + + # Check if immich_server is paused and unpause it if needed + if [ "${IMMICH_SERVER_RUNNING:-true}" = true ] && docker inspect --format='{{.State.Status}}' immich_server 2>/dev/null | grep -q "paused"; then + echo "Unpausing immich_server container during cleanup..." + docker unpause immich_server 2>/dev/null || true + fi + + if [ $exit_code -ne 0 ]; then + echo "Script failed with exit code $exit_code" + send_notification "🚨 Immich Backup Failed" "Backup process encountered an error (exit code: $exit_code)" "error" + fi + + exit $exit_code +} + +# Set up trap to call cleanup function on script exit (normal or error) +trap cleanup EXIT SIGINT SIGTERM + +# Load environment variables from the .env file +ENV_FILE="$(dirname "$0")/../.env" +if [ -f "$ENV_FILE" ]; then + echo "Loading environment variables from $ENV_FILE" + source "$ENV_FILE" +else + echo "Error: .env file not found in $(dirname "$0")/.." + exit 1 +fi + +# Verify required environment variables are set +if [ -z "$DB_USERNAME" ] || [ -z "$DB_DATABASE_NAME" ] || [ -z "$UPLOAD_LOCATION" ]; then + echo "Error: Required environment variables (DB_USERNAME, DB_DATABASE_NAME, UPLOAD_LOCATION) not found in .env file" + echo "Please ensure your .env file contains:" + echo " - DB_USERNAME=" + echo " - DB_DATABASE_NAME=" + echo " - UPLOAD_LOCATION=" + exit 1 +fi + +# B2 CLI tool path +B2_CLI="$(dirname "$0")/b2-linux" + +# Notification function +send_notification() { + local title="$1" + local message="$2" + local status="${3:-info}" # success, error, warning, info + local hostname=$(hostname) + + # Console notification + log_message "$title: $message" + + # Webhook notification + if [ -n "$WEBHOOK_URL" ]; then + local tags="backup,immich,${hostname}" + [ "$status" == "error" ] && tags="${tags},errors" + [ "$status" == "warning" ] && tags="${tags},warnings" + + # Clean message without newlines or timestamps for webhook + local webhook_message="$message" + + curl -s \ + -H "tags:${tags}" \ + -d "$webhook_message" \ + "$WEBHOOK_URL" 2>/dev/null || log_message "Warning: Failed to send webhook notification" + fi +} + +# Function to upload to Backblaze B2 +upload_to_b2() { + local file_path="$1" + local filename=$(basename "$file_path") + + # Check if B2 is configured + if [ -z "$B2_APPLICATION_KEY_ID" ] || [ -z "$B2_APPLICATION_KEY" ] || [ -z "$B2_BUCKET_NAME" ]; then + log_message "B2 upload skipped: B2 credentials not configured in .env file" + return 0 + fi + + # Check if B2 CLI exists + if [ ! -f "$B2_CLI" ]; then + log_message "Error: B2 CLI not found at $B2_CLI" + return 1 + fi + + log_message "Uploading $filename to B2 bucket: $B2_BUCKET_NAME" + + # Authorize B2 account + if ! "$B2_CLI" authorize-account "$B2_APPLICATION_KEY_ID" "$B2_APPLICATION_KEY" 2>/dev/null; then + log_message "Error: Failed to authorize B2 account" + return 1 + fi + + # Upload file to B2 + if "$B2_CLI" upload-file "$B2_BUCKET_NAME" "$file_path" "immich-backups/$filename" 2>/dev/null; then + log_message "✅ Successfully uploaded $filename to B2" + return 0 + else + log_message "❌ Failed to upload $filename to B2" + return 1 + fi +} + +# Initialize container status variables +IMMICH_SERVER_RUNNING=true + +# Set up logging to central logs directory +LOG_DIR="$(dirname "$0")/../logs" +mkdir -p "$LOG_DIR" +LOG_FILE="${LOG_DIR}/immich-backup.log" + +# Function to log with timestamp +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# Function to log without timestamp (for progress/status) +log_status() { + echo "$1" | tee -a "$LOG_FILE" +} + +# Create backup directory if it doesn't exist +BACKUP_DIR="$(dirname "$0")/../immich_backups" +mkdir -p "$BACKUP_DIR" + +# Generate timestamp for the backup filename +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +DB_BACKUP_FILENAME="immich_db_backup_${TIMESTAMP}.sql" +DB_BACKUP_PATH="${BACKUP_DIR}/${DB_BACKUP_FILENAME}" +UPLOAD_BACKUP_PATH="${BACKUP_DIR}/immich_uploads_${TIMESTAMP}.tar.gz" + +log_message "Starting complete backup of Immich installation..." +log_message "Using settings from .env file:" +log_message " - Database: ${DB_DATABASE_NAME}" +log_message " - Username: ${DB_USERNAME}" +log_message " - Upload Location: ${UPLOAD_LOCATION}" +log_message " - Container: immich_postgres" +log_message " - Backup Directory: ${BACKUP_DIR}" + +# Send start notification +send_notification "🚀 Immich Backup Started" "Starting complete backup of Immich database and uploads directory" "info" + +# Check if the Immich server container exists and is running +log_status "Checking immich_server container status..." +if docker ps -q --filter "name=immich_server" | grep -q .; then + log_message "Pausing immich_server container to minimize database writes..." + if ! docker pause immich_server; then + log_message "Failed to pause immich_server container." + # Continue with backup instead of exiting + fi +else + log_message "Note: immich_server container not found or not running. Continuing with backup anyway." + # Set a flag so we don't try to unpause it later + IMMICH_SERVER_RUNNING=false +fi + +# Check if the Postgres container exists and is running +log_status "Checking postgres container status..." +if ! docker ps -q --filter "name=immich_postgres" | grep -q .; then + log_message "Error: immich_postgres container is not running. Cannot proceed with backup." + exit 1 +fi + +# Check if the Immich server container exists and is running +log_status "Checking immich_server container status..." +if docker ps -q --filter "name=immich_server" | grep -q .; then + log_message "Pausing immich_server container to minimize database writes during backup..." + if ! docker pause immich_server; then + log_message "Failed to pause immich_server container." + # Continue with backup instead of exiting + fi +else + log_message "Note: immich_server container not found or not running. Continuing with backup anyway." + # Set a flag so we don't try to unpause it later + IMMICH_SERVER_RUNNING=false +fi + +echo "" +echo "=== PHASE 1: DATABASE BACKUP ===" +log_message "Taking database backup using pg_dumpall as recommended by Immich documentation..." +# Use pg_dumpall with recommended flags: --clean and --if-exists +docker exec -t immich_postgres pg_dumpall \ + --clean \ + --if-exists \ + --username="${DB_USERNAME}" \ + > "${DB_BACKUP_PATH}" + +# Check if the dump was successful +if [ $? -ne 0 ] || [ ! -s "${DB_BACKUP_PATH}" ]; then + log_message "Error: Database backup failed or created an empty file." + exit 1 +fi + +log_message "Database backup completed successfully!" + +# Compress the database backup file +log_message "Compressing database backup file..." +if ! gzip -f "${DB_BACKUP_PATH}"; then + log_message "Warning: Failed to compress database backup file." +fi + +echo "" +echo "=== PHASE 2: UPLOAD DIRECTORY BACKUP ===" +log_message "Backing up user upload directory: ${UPLOAD_LOCATION}" + +# Verify the upload location exists +if [ ! -d "${UPLOAD_LOCATION}" ]; then + log_message "Error: Upload location ${UPLOAD_LOCATION} does not exist!" + exit 1 +fi + +# Create compressed archive of the upload directory +# According to Immich docs, we need to backup the entire UPLOAD_LOCATION +# which includes: upload/, profile/, thumbs/, encoded-video/, library/, backups/ +log_message "Creating compressed archive of upload directory..." +log_message "This may take a while depending on the size of your media library..." + +# Use tar with progress indication and exclude any existing backup files in the upload location +if ! tar --exclude="${UPLOAD_LOCATION}/backups/*.tar.gz" \ + --exclude="${UPLOAD_LOCATION}/backups/*.sql.gz" \ + -czf "${UPLOAD_BACKUP_PATH}" \ + -C "$(dirname "${UPLOAD_LOCATION}")" \ + "$(basename "${UPLOAD_LOCATION}")"; then + log_message "Error: Failed to create upload directory backup." + exit 1 +fi + +log_message "Upload directory backup completed successfully!" + +# Resume the Immich server only if it was running and we paused it +if [ "${IMMICH_SERVER_RUNNING:-true}" = true ]; then + log_status "Resuming immich_server container..." + if ! docker unpause immich_server 2>/dev/null; then + log_message "Note: No need to unpause immich_server container." + fi +fi + +# Resume the Immich server only if it was running and we paused it +if [ "${IMMICH_SERVER_RUNNING:-true}" = true ]; then + log_status "Resuming immich_server container..." + if ! docker unpause immich_server 2>/dev/null; then + log_message "Note: No need to unpause immich_server container." + fi +fi + +echo "" +echo "=== BACKUP COMPLETED SUCCESSFULLY! ===" +echo "Database backup saved to: ${DB_BACKUP_PATH}.gz" +echo "Upload directory backup saved to: ${UPLOAD_BACKUP_PATH}" + +# Calculate backup sizes +DB_BACKUP_SIZE=$(du -h "${DB_BACKUP_PATH}.gz" 2>/dev/null | cut -f1 || echo "Unknown") +UPLOAD_BACKUP_SIZE=$(du -h "${UPLOAD_BACKUP_PATH}" 2>/dev/null | cut -f1 || echo "Unknown") + +echo "" +echo "=== BACKUP SUMMARY ===" +echo "Database backup size: ${DB_BACKUP_SIZE}" +echo "Upload directory backup size: ${UPLOAD_BACKUP_SIZE}" + +# Upload to B2 (if configured) +echo "" +echo "=== UPLOADING TO BACKBLAZE B2 ===" +B2_UPLOAD_SUCCESS=true + +# Upload database backup +if ! upload_to_b2 "${DB_BACKUP_PATH}.gz"; then + B2_UPLOAD_SUCCESS=false +fi + +# Upload uploads backup +if ! upload_to_b2 "${UPLOAD_BACKUP_PATH}"; then + B2_UPLOAD_SUCCESS=false +fi + +# Prepare notification message +DB_FILENAME=$(basename "${DB_BACKUP_PATH}.gz") +UPLOAD_FILENAME=$(basename "${UPLOAD_BACKUP_PATH}") +NOTIFICATION_MESSAGE="📦 Database: ${DB_FILENAME} (${DB_BACKUP_SIZE}) +📁 Uploads: ${UPLOAD_FILENAME} (${UPLOAD_BACKUP_SIZE})" + +if [ "$B2_UPLOAD_SUCCESS" = true ] && [ -n "$B2_BUCKET_NAME" ]; then + NOTIFICATION_MESSAGE="${NOTIFICATION_MESSAGE} +☁️ Successfully uploaded to B2 bucket: ${B2_BUCKET_NAME}" + send_notification "✅ Immich Backup Completed" "$NOTIFICATION_MESSAGE" "success" +elif [ -n "$B2_BUCKET_NAME" ]; then + NOTIFICATION_MESSAGE="${NOTIFICATION_MESSAGE} +⚠️ B2 upload failed - files saved locally only" + send_notification "⚠️ Immich Backup Completed (B2 Upload Failed)" "$NOTIFICATION_MESSAGE" "warning" +else + send_notification "✅ Immich Backup Completed" "$NOTIFICATION_MESSAGE" "success" +fi + +# Show backup information +echo "" +echo "=== BACKUP INVENTORY ===" +find "${BACKUP_DIR}" -name "*.gz" | wc -l | xargs echo "Total number of backup files:" +du -sh "${BACKUP_DIR}" | cut -f1 | xargs echo "Total backup directory size:" + +# List recent backups +echo "" +echo "Recent backups:" +find "${BACKUP_DIR}" -name "*.gz" -mtime -7 | sort + +# Health check: Verify backup file sizes +DB_BACKUP_SIZE_KB=$(du -k "${DB_BACKUP_PATH}.gz" 2>/dev/null | cut -f1 || echo "0") +UPLOAD_BACKUP_SIZE_KB=$(du -k "${UPLOAD_BACKUP_PATH}" 2>/dev/null | cut -f1 || echo "0") + +echo "" +echo "=== BACKUP VALIDATION ===" +if [ "${DB_BACKUP_SIZE_KB}" -lt 100 ]; then + echo "WARNING: Database backup file is smaller than expected (${DB_BACKUP_SIZE_KB}KB). Please verify its integrity." +else + echo "✓ Database backup size appears normal (${DB_BACKUP_SIZE_KB}KB)" +fi + +if [ "${UPLOAD_BACKUP_SIZE_KB}" -lt 1024 ]; then + echo "WARNING: Upload directory backup file is smaller than expected (${UPLOAD_BACKUP_SIZE_KB}KB). Please verify its integrity." +else + echo "✓ Upload directory backup size appears normal (${UPLOAD_BACKUP_SIZE_KB}KB)" +fi + +# Optional: Remove old backups (older than 30 days) +echo "" +echo "=== CLEANUP ===" +OLD_BACKUPS=$(find "${BACKUP_DIR}" -name "*.gz" -mtime +30 | wc -l) +if [ "${OLD_BACKUPS}" -gt 0 ]; then + echo "Found ${OLD_BACKUPS} backup files older than 30 days." + echo "To remove them automatically, uncomment the cleanup line in this script." + # Uncomment the next line to automatically remove old backups + # find "${BACKUP_DIR}" -name "*.gz" -mtime +30 -delete +else + echo "No old backup files found (older than 30 days)." +fi + +echo "" +echo "=== RESTORE INSTRUCTIONS ===" +echo "To restore from this backup:" +echo "1. Database restore instructions: https://immich.app/docs/administration/backup-and-restore/#database" +echo "2. Upload directory: Extract ${UPLOAD_BACKUP_PATH} to your UPLOAD_LOCATION" +echo "" +echo "IMPORTANT: For a complete restore, you need BOTH the database backup AND the upload directory backup." +echo "The database contains metadata, while the upload directory contains your actual photos and videos." diff --git a/immich/restore-immich.sh b/immich/restore-immich.sh new file mode 100755 index 0000000..79c7378 --- /dev/null +++ b/immich/restore-immich.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Immich Restore Script +# This script restores an Immich installation from backups created by backup-immich.sh +# Based on recommendations from https://immich.app/docs/administration/backup-and-restore/ + +# Set up error handling +set -e + +# Load environment variables from the .env file +ENV_FILE="$(dirname "$0")/../.env" +if [ -f "$ENV_FILE" ]; then + echo "Loading environment variables from $ENV_FILE" + source "$ENV_FILE" +else + echo "Error: .env file not found in $(dirname "$0")/.." + exit 1 +fi + +# Set up logging to central logs directory +LOG_DIR="$(dirname "$0")/../logs" +mkdir -p "$LOG_DIR" +LOG_FILE="${LOG_DIR}/immich-restore.log" + +# Function to log with timestamp +log_message() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# Function to display usage +usage() { + echo "Usage: $0 --db-backup --uploads-backup [options]" + echo "" + echo "Required arguments:" + echo " --db-backup PATH Path to database backup file (.sql.gz)" + echo " --uploads-backup PATH Path to uploads backup file (.tar.gz)" + echo "" + echo "Optional arguments:" + echo " --dry-run Show what would be restored without making changes" + echo " --skip-db Skip database restoration" + echo " --skip-uploads Skip uploads restoration" + echo " --help Show this help message" + echo "" + echo "Example:" + echo " $0 --db-backup ./immich_backups/immich_db_backup_20250526_120000.sql.gz \\" + echo " --uploads-backup ./immich_backups/immich_uploads_20250526_120000.tar.gz" +} + +# Parse command line arguments +DB_BACKUP="" +UPLOADS_BACKUP="" +DRY_RUN=false +SKIP_DB=false +SKIP_UPLOADS=false + +while [[ $# -gt 0 ]]; do + case $1 in + --db-backup) + DB_BACKUP="$2" + shift 2 + ;; + --uploads-backup) + UPLOADS_BACKUP="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --skip-db) + SKIP_DB=true + shift + ;; + --skip-uploads) + SKIP_UPLOADS=true + shift + ;; + --help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Validate required arguments +if [ -z "$DB_BACKUP" ] && [ "$SKIP_DB" = false ]; then + echo "Error: --db-backup is required unless --skip-db is specified" + usage + exit 1 +fi + +if [ -z "$UPLOADS_BACKUP" ] && [ "$SKIP_UPLOADS" = false ]; then + echo "Error: --uploads-backup is required unless --skip-uploads is specified" + usage + exit 1 +fi + +# Validate backup files exist +if [ "$SKIP_DB" = false ] && [ ! -f "$DB_BACKUP" ]; then + echo "Error: Database backup file not found: $DB_BACKUP" + exit 1 +fi + +if [ "$SKIP_UPLOADS" = false ] && [ ! -f "$UPLOADS_BACKUP" ]; then + echo "Error: Uploads backup file not found: $UPLOADS_BACKUP" + exit 1 +fi + +echo "=== IMMICH RESTORE OPERATION ===" +echo "Database backup: ${DB_BACKUP:-SKIPPED}" +echo "Uploads backup: ${UPLOADS_BACKUP:-SKIPPED}" +echo "Dry run mode: $DRY_RUN" +echo "" + +if [ "$DRY_RUN" = true ]; then + echo "DRY RUN MODE - No changes will be made" + echo "" +fi + +# TODO: Implement restore logic +echo "⚠️ RESTORE SCRIPT TEMPLATE ⚠️" +echo "" +echo "This is a template script. Implementation needed:" +echo "" +echo "1. Stop Immich containers" +echo "2. Restore database (if not skipped):" +echo " - Decompress $DB_BACKUP" +echo " - Execute SQL restore commands" +echo "3. Restore uploads (if not skipped):" +echo " - Extract $UPLOADS_BACKUP to $UPLOAD_LOCATION" +echo " - Set proper ownership and permissions" +echo "4. Restart Immich containers" +echo "5. Verify restoration" +echo "" +echo "For detailed restore instructions, see:" +echo "https://immich.app/docs/administration/backup-and-restore/" diff --git a/immich/validate-immich-backups.sh b/immich/validate-immich-backups.sh new file mode 100755 index 0000000..cd08f61 --- /dev/null +++ b/immich/validate-immich-backups.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +# Immich Backup Validation Script +# This script validates Immich backup files for integrity and completeness + +# Set up error handling +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to display usage +usage() { + echo "Usage: $0 [backup_directory]" + echo "" + echo "Arguments:" + echo " backup_directory Directory containing backup files (default: ../immich_backups)" + echo "" + echo "This script validates:" + echo " - Database backup file integrity" + echo " - Upload archive integrity" + echo " - File sizes and timestamps" + echo " - Backup completeness" +} + +# Parse command line arguments +BACKUP_DIR="${1:-$(dirname "$0")/../immich_backups}" + +# Set up logging to central logs directory +LOG_DIR="$(dirname "$0")/../logs" +mkdir -p "$LOG_DIR" +LOG_FILE="${LOG_DIR}/immich-validation.log" + +# Function to log validation results +log_validation() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" +} + +# Verify backup directory exists +if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Error: Backup directory not found: $BACKUP_DIR${NC}" + log_validation "Error: Backup directory not found: $BACKUP_DIR" + exit 1 +fi + +log_validation "Starting backup validation for directory: $BACKUP_DIR" +echo "=== IMMICH BACKUP VALIDATION ===" +echo "Backup directory: $BACKUP_DIR" +echo "" + +# Find backup files +DB_BACKUPS=$(find "$BACKUP_DIR" -name "immich_db_backup_*.sql.gz" -type f | sort -r) +UPLOAD_BACKUPS=$(find "$BACKUP_DIR" -name "immich_uploads_*.tar.gz" -type f | sort -r) + +TOTAL_ERRORS=0 + +echo "=== DATABASE BACKUPS ===" +if [ -z "$DB_BACKUPS" ]; then + echo -e "${YELLOW}Warning: No database backup files found${NC}" + log_validation "Warning: No database backup files found" + ((TOTAL_ERRORS++)) +else + for backup in $DB_BACKUPS; do + echo "Validating: $(basename "$backup")" + + # Check file size + SIZE=$(stat -c%s "$backup" 2>/dev/null || echo "0") + if [ "$SIZE" -lt 1024 ]; then + echo -e " ${RED}✗ File is too small (${SIZE} bytes)${NC}" + log_validation "Error: File is too small (${SIZE} bytes) - $(basename "$backup")" + ((TOTAL_ERRORS++)) + else + echo -e " ${GREEN}✓ File size OK ($(du -h "$backup" | cut -f1))${NC}" + fi + + # Check if it's a valid gzip file + if gzip -t "$backup" 2>/dev/null; then + echo -e " ${GREEN}✓ Gzip file integrity OK${NC}" + else + echo -e " ${RED}✗ Gzip file corruption detected${NC}" + log_validation "Error: Gzip file corruption detected - $(basename "$backup")" + ((TOTAL_ERRORS++)) + fi + + # Check if SQL content looks valid (basic check) + if zcat "$backup" 2>/dev/null | head -n 10 | grep -q "PostgreSQL database dump"; then + echo -e " ${GREEN}✓ SQL content appears valid${NC}" + else + echo -e " ${YELLOW}? Cannot verify SQL content format${NC}" + fi + + echo "" + done +fi + +echo "=== UPLOAD BACKUPS ===" +if [ -z "$UPLOAD_BACKUPS" ]; then + echo -e "${YELLOW}Warning: No upload backup files found${NC}" + log_validation "Warning: No upload backup files found" + ((TOTAL_ERRORS++)) +else + for backup in $UPLOAD_BACKUPS; do + echo "Validating: $(basename "$backup")" + + # Check file size + SIZE=$(stat -c%s "$backup" 2>/dev/null || echo "0") + if [ "$SIZE" -lt 1024 ]; then + echo -e " ${RED}✗ File is too small (${SIZE} bytes)${NC}" + log_validation "Error: File is too small (${SIZE} bytes) - $(basename "$backup")" + ((TOTAL_ERRORS++)) + else + echo -e " ${GREEN}✓ File size OK ($(du -h "$backup" | cut -f1))${NC}" + fi + + # Check if it's a valid tar.gz file + if tar -tzf "$backup" >/dev/null 2>&1; then + echo -e " ${GREEN}✓ Tar.gz file integrity OK${NC}" + + # Count files in archive + FILE_COUNT=$(tar -tzf "$backup" 2>/dev/null | wc -l) + echo -e " ${GREEN}✓ Archive contains ${FILE_COUNT} files/directories${NC}" + else + echo -e " ${RED}✗ Tar.gz file corruption detected${NC}" + log_validation "Error: Tar.gz file corruption detected - $(basename "$backup")" + ((TOTAL_ERRORS++)) + fi + + echo "" + done +fi + +echo "=== BACKUP PAIRING VALIDATION ===" +# Check if we have matching pairs of backups (same timestamp) +DB_TIMESTAMPS=$(echo "$DB_BACKUPS" | sed 's/.*immich_db_backup_\([0-9_]*\)\.sql\.gz/\1/' | sort) +UPLOAD_TIMESTAMPS=$(echo "$UPLOAD_BACKUPS" | sed 's/.*immich_uploads_\([0-9_]*\)\.tar\.gz/\1/' | sort) + +echo "Database backup timestamps: $(echo "$DB_TIMESTAMPS" | tr '\n' ' ')" +echo "Upload backup timestamps: $(echo "$UPLOAD_TIMESTAMPS" | tr '\n' ' ')" + +# Find matching pairs +MATCHED_PAIRS=0 +for db_ts in $DB_TIMESTAMPS; do + if echo "$UPLOAD_TIMESTAMPS" | grep -q "^${db_ts}$"; then + echo -e "${GREEN}✓ Complete backup set found for timestamp: $db_ts${NC}" + ((MATCHED_PAIRS++)) + else + echo -e "${YELLOW}? Incomplete backup set for timestamp: $db_ts (missing upload backup)${NC}" + log_validation "Warning: Incomplete backup set for timestamp: $db_ts (missing upload backup)" + fi +done + +for upload_ts in $UPLOAD_TIMESTAMPS; do + if ! echo "$DB_TIMESTAMPS" | grep -q "^${upload_ts}$"; then + echo -e "${YELLOW}? Incomplete backup set for timestamp: $upload_ts (missing database backup)${NC}" + log_validation "Warning: Incomplete backup set for timestamp: $upload_ts (missing database backup)" + fi +done + +echo "" +echo "=== VALIDATION SUMMARY ===" +echo "Complete backup pairs: $MATCHED_PAIRS" +echo "Total validation errors: $TOTAL_ERRORS" + +log_validation "Validation summary: $MATCHED_PAIRS complete backup pairs, $TOTAL_ERRORS errors" + +if [ "$TOTAL_ERRORS" -eq 0 ]; then + echo -e "${GREEN}✓ All backup validations passed${NC}" + log_validation "Success: All backup validations passed" + exit 0 +else + echo -e "${RED}✗ Backup validation failed with $TOTAL_ERRORS errors${NC}" + log_validation "Error: Backup validation failed with $TOTAL_ERRORS errors" + exit 1 +fi