diff --git a/immich/backup-immich.sh b/immich/backup-immich.sh index 6e5efe4..083228f 100755 --- a/immich/backup-immich.sh +++ b/immich/backup-immich.sh @@ -53,13 +53,13 @@ cleanup() { trap cleanup EXIT SIGINT SIGTERM # Load environment variables from the .env file -ENV_FILE="$(dirname "$0")/../.env" +ENV_FILE="${SCRIPT_DIR}/../.env" if [ -f "$ENV_FILE" ]; then echo "Loading environment variables from $ENV_FILE" # shellcheck source=/dev/null source "$ENV_FILE" else - echo "Error: .env file not found in $(dirname "$0")/.." + echo "Error: .env file not found in ${SCRIPT_DIR}/.." exit 1 fi @@ -109,6 +109,7 @@ EXAMPLES: $(basename "$0") --help # Show this help $(basename "$0") --dry-run # Preview backup without executing $(basename "$0") --no-upload # Backup locally only (skip B2) + $(basename "$0") --upload-only # Only upload the latest existing backup to B2 RESTORE INSTRUCTIONS: https://immich.app/docs/administration/backup-and-restore/ @@ -119,6 +120,7 @@ EOF # Parse command line arguments DRY_RUN=false NO_UPLOAD=false +UPLOAD_ONLY=false VERBOSE=false while [[ $# -gt 0 ]]; do @@ -135,6 +137,10 @@ while [[ $# -gt 0 ]]; do NO_UPLOAD=true shift ;; + --upload-only) + UPLOAD_ONLY=true + shift + ;; --verbose) VERBOSE=true shift @@ -148,12 +154,12 @@ while [[ $# -gt 0 ]]; do done # B2 CLI tool path -if [ -f "$(dirname "$0")/b2-linux" ]; then - B2_CLI="$(dirname "$0")/b2-linux" +if [ -f "${SCRIPT_DIR}/b2-linux" ]; then + B2_CLI="${SCRIPT_DIR}/b2-linux" elif command -v b2 &> /dev/null; then B2_CLI=$(command -v b2) else - B2_CLI="$(dirname "$0")/b2-linux" + B2_CLI="${SCRIPT_DIR}/b2-linux" fi # Notification function @@ -204,17 +210,40 @@ upload_to_b2() { 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 + local auth_output + if ! auth_output=$("$B2_CLI" authorize-account "$B2_APPLICATION_KEY_ID" "$B2_APPLICATION_KEY" 2>&1); then log_message "Error: Failed to authorize B2 account" + log_message "B2 Output: $auth_output" return 1 fi # Upload file to B2 - if "$B2_CLI" upload-file "$B2_BUCKET_NAME" "$file_path" "immich-backups/$filename" 2>/dev/null; then + local temp_log + temp_log=$(mktemp) + + # Enable pipefail to catch b2 exit code through tee + set -o pipefail + + # Use --threads 4 to avoid "More than one concurrent upload using auth token" error + # which can happen with default thread count on large files + if "$B2_CLI" file upload --threads 4 "$B2_BUCKET_NAME" "$file_path" "immich-backups/$filename" 2>&1 | tee "$temp_log"; then + set +o pipefail + rm "$temp_log" log_message "✅ Successfully uploaded $filename to B2" return 0 else + local exit_code=$? + set +o pipefail log_message "❌ Failed to upload $filename to B2" + + # Log the last few lines of output to capture the error message + # avoiding the progress bar spam + local error_msg + error_msg=$(tail -n 20 "$temp_log") + log_message "B2 Output (last 20 lines):" + log_message "$error_msg" + + rm "$temp_log" return 1 fi } @@ -223,7 +252,7 @@ upload_to_b2() { IMMICH_SERVER_RUNNING=true # Set up logging to central logs directory -LOG_DIR="$(dirname "$0")/../logs" +LOG_DIR="${SCRIPT_DIR}/../logs" mkdir -p "$LOG_DIR" LOG_FILE="${LOG_DIR}/immich-backup.log" @@ -243,10 +272,35 @@ mkdir -p "$BACKUP_DIR" # Shared backup directory (can be overridden in .env) SHARED_BACKUP_DIR="${SHARED_BACKUP_DIR:-/mnt/share/media/backups/immich}" -# Generate timestamp for the backup filename +# Handle upload-only mode +if [ "$UPLOAD_ONLY" = true ]; then + log_message "=== UPLOAD ONLY MODE ===" + log_message "Skipping backup creation, looking for latest backups in $SHARED_BACKUP_DIR" + + # Find latest database backup + LATEST_DB=$(ls -t "$SHARED_BACKUP_DIR"/immich_db_backup_*.sql.gz 2>/dev/null | head -n1) + if [ -f "$LATEST_DB" ]; then + log_message "Found latest database backup: $LATEST_DB" + upload_to_b2 "$LATEST_DB" + else + log_message "Warning: No database backup found in $SHARED_BACKUP_DIR" + fi + + # Find latest uploads backup + LATEST_UPLOADS=$(ls -t "$SHARED_BACKUP_DIR"/immich_uploads_*.tar.gz 2>/dev/null | head -n1) + if [ -f "$LATEST_UPLOADS" ]; then + log_message "Found latest uploads backup: $LATEST_UPLOADS" + upload_to_b2 "$LATEST_UPLOADS" + else + log_message "Warning: No uploads backup found in $SHARED_BACKUP_DIR" + fi + + log_message "Upload only mode completed." + exit 0 +fi # Create backup directory if it doesn't exist -BACKUP_DIR="$(dirname "$0")/../immich_backups" +BACKUP_DIR="${SCRIPT_DIR}/../immich_backups" mkdir -p "$BACKUP_DIR" # Generate timestamp for the backup filename @@ -446,8 +500,9 @@ 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" \ +# Note: Exclude patterns must match the relative path structure used by -C +if ! tar --exclude="$(basename "${UPLOAD_LOCATION}")/backups/*.tar.gz" \ + --exclude="$(basename "${UPLOAD_LOCATION}")/backups/*.sql.gz" \ -czf "${UPLOAD_BACKUP_PATH}" \ -C "$(dirname "${UPLOAD_LOCATION}")" \ "$(basename "${UPLOAD_LOCATION}")"; then