From 547372fae7f3c1890ee75dd7ce88840ffda7fa08 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 14:38:35 +0000 Subject: [PATCH 1/2] Add R2 backup system for database and images Implements cloud backup to Cloudflare R2 with tiered retention: - Daily backups: kept for 7 days - Weekly backups (Sundays): kept for 12 weeks - Monthly backups (1st of month): kept for 12 months Key features: - Images synced incrementally using rclone (no duplicates) - Database compressed with gzip before upload - Easy restore script with integrity checks - Setup script for rclone configuration - Systemd timer runs daily at 3 AM --- deploy/README.md | 72 +++++++- deploy/dogbook-r2-backup.service | 18 ++ deploy/dogbook-r2-backup.timer | 13 ++ deploy/r2-backup.sh | 183 +++++++++++++++++++ deploy/r2-restore.sh | 305 +++++++++++++++++++++++++++++++ deploy/setup-r2-backup.sh | 174 ++++++++++++++++++ 6 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 deploy/dogbook-r2-backup.service create mode 100644 deploy/dogbook-r2-backup.timer create mode 100644 deploy/r2-backup.sh create mode 100644 deploy/r2-restore.sh create mode 100644 deploy/setup-r2-backup.sh diff --git a/deploy/README.md b/deploy/README.md index 163adf7..c27cdf1 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -29,6 +29,76 @@ This will: ## Backup Strategy +### Local Backups (Legacy) + - Daily SQLite database backups at `/srv/dogbook/backups/` - Keeps last 30 days of backups -- Images are NOT backed up (stored in `/srv/dogbook/data/images/`) +- Run by `dogbook-backup.timer` + +### R2 Cloud Backups (Recommended) + +Full backup of database AND images to Cloudflare R2 with tiered retention: + +- **Daily backups**: kept for 7 days +- **Weekly backups** (Sundays): kept for 12 weeks (~3 months) +- **Monthly backups** (1st of month): kept for 12 months (1 year) +- **Images**: synced incrementally (no duplicates - only uploads new/changed files) + +**Files:** +- `r2-backup.sh` - Main backup script +- `r2-restore.sh` - Restore from backup +- `setup-r2-backup.sh` - One-time setup script +- `dogbook-r2-backup.service` - Systemd service +- `dogbook-r2-backup.timer` - Runs daily at 3 AM + +**Setup R2 Backups:** + +```bash +# Run the setup script (configures rclone and installs timer) +sudo ./deploy/setup-r2-backup.sh +``` + +You'll need: +1. Cloudflare R2 bucket created +2. R2 API token with read/write access +3. Account ID (from dashboard URL) + +**Manual Commands:** + +```bash +# Check backup status +systemctl status dogbook-r2-backup.timer + +# Manually trigger a backup +sudo systemctl start dogbook-r2-backup.service + +# View backup logs +journalctl -u dogbook-r2-backup.service + +# List all backups +sudo -u dogbook /srv/dogbook/deploy/r2-backup.sh list +``` + +**Restore from Backup:** + +```bash +# List available backups +sudo /srv/dogbook/deploy/r2-restore.sh list + +# Restore latest database backup +sudo /srv/dogbook/deploy/r2-restore.sh latest + +# Restore database from specific date +sudo /srv/dogbook/deploy/r2-restore.sh db 2024-01-15 + +# Restore images +sudo /srv/dogbook/deploy/r2-restore.sh images + +# Full restore (database + images) +sudo /srv/dogbook/deploy/r2-restore.sh all +``` + +**Estimated Storage Usage:** +- Database: ~30-90 MB (31 copies with tiered retention) +- Images: ~300 MB - 1.5 GB (stored once, synced incrementally) +- Total: ~400 MB - 2 GB initially, growing with usage diff --git a/deploy/dogbook-r2-backup.service b/deploy/dogbook-r2-backup.service new file mode 100644 index 0000000..61c567d --- /dev/null +++ b/deploy/dogbook-r2-backup.service @@ -0,0 +1,18 @@ +[Unit] +Description=Backup Dogbook Database and Images to R2 +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +User=dogbook +ExecStart=/srv/dogbook/deploy/r2-backup.sh backup +StandardOutput=journal +StandardError=journal + +# Allow up to 30 minutes for the backup (in case of large image sync) +TimeoutStartSec=1800 + +# Retry on failure +Restart=on-failure +RestartSec=60 diff --git a/deploy/dogbook-r2-backup.timer b/deploy/dogbook-r2-backup.timer new file mode 100644 index 0000000..b616fa9 --- /dev/null +++ b/deploy/dogbook-r2-backup.timer @@ -0,0 +1,13 @@ +[Unit] +Description=Daily Dogbook R2 Backup Timer + +[Timer] +# Run at 3:00 AM daily (off-peak hours) +OnCalendar=*-*-* 03:00:00 +# Run immediately if last run was missed (e.g., server was down) +Persistent=true +# Add random delay up to 15 minutes to avoid thundering herd +RandomizedDelaySec=900 + +[Install] +WantedBy=timers.target diff --git a/deploy/r2-backup.sh b/deploy/r2-backup.sh new file mode 100644 index 0000000..246e632 --- /dev/null +++ b/deploy/r2-backup.sh @@ -0,0 +1,183 @@ +#!/bin/bash +# R2 Backup Script for Dogbook +# Backs up SQLite database and images to Cloudflare R2 +# +# Retention policy: +# - Daily backups: kept for 7 days +# - Weekly backups (Sundays): kept for 3 months (~12 weeks) +# - Monthly backups (1st of month): kept for 1 year (~12 months) +# +# Images are synced incrementally (no duplicates - only uploads new/changed files) + +set -euo pipefail + +# Configuration +DATA_DIR="/srv/dogbook/data" +DB_FILE="$DATA_DIR/keystone.db" +IMAGES_DIR="$DATA_DIR/images" +TMP_DIR="/tmp/dogbook-backup" +R2_BUCKET="maisonsdoggo" + +# Date calculations +TODAY=$(date +%Y-%m-%d) +DAY_OF_WEEK=$(date +%u) # 1=Monday, 7=Sunday +DAY_OF_MONTH=$(date +%d) + +# Logging +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +error() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2 +} + +# Check prerequisites +check_prerequisites() { + if ! command -v rclone &> /dev/null; then + error "rclone is not installed. Install with: curl https://rclone.org/install.sh | sudo bash" + exit 1 + fi + + if ! rclone listremotes | grep -q "^r2:$"; then + error "rclone remote 'r2' is not configured. Run: rclone config" + exit 1 + fi + + if [ ! -f "$DB_FILE" ]; then + error "Database file not found: $DB_FILE" + exit 1 + fi +} + +# Create a safe SQLite backup using .backup command +backup_database() { + log "Backing up SQLite database..." + + mkdir -p "$TMP_DIR" + local backup_file="$TMP_DIR/keystone-$TODAY.db" + local compressed_file="$backup_file.gz" + + # Use SQLite's .backup command for a consistent backup + sqlite3 "$DB_FILE" ".backup '$backup_file'" + + # Compress the backup + gzip -f "$backup_file" + + log "Database backup created: $compressed_file ($(du -h "$compressed_file" | cut -f1))" + + # Upload to daily folder + log "Uploading daily backup..." + rclone copyto "$compressed_file" "r2:$R2_BUCKET/db/daily/keystone-$TODAY.db.gz" + + # Also keep a 'latest' copy for easy restore + rclone copyto "$compressed_file" "r2:$R2_BUCKET/latest/keystone.db.gz" + + # Weekly backup (Sunday) + if [ "$DAY_OF_WEEK" -eq 7 ]; then + log "Creating weekly backup (Sunday)..." + rclone copyto "$compressed_file" "r2:$R2_BUCKET/db/weekly/keystone-$TODAY.db.gz" + fi + + # Monthly backup (1st of month) + if [ "$DAY_OF_MONTH" -eq "01" ]; then + log "Creating monthly backup (1st of month)..." + rclone copyto "$compressed_file" "r2:$R2_BUCKET/db/monthly/keystone-$TODAY.db.gz" + fi + + # Clean up temp file + rm -f "$compressed_file" +} + +# Sync images to R2 (incremental - only uploads new/changed files) +sync_images() { + if [ ! -d "$IMAGES_DIR" ]; then + log "Images directory not found, skipping image sync" + return + fi + + log "Syncing images to R2 (incremental)..." + + # Use rclone sync with checksum to avoid re-uploading unchanged files + # --checksum: Compare by checksum instead of mod-time/size (more reliable) + # --progress: Show transfer progress + rclone sync "$IMAGES_DIR" "r2:$R2_BUCKET/images/" \ + --checksum \ + --transfers 4 \ + --stats-one-line \ + -v + + log "Image sync complete" +} + +# Apply retention policy - delete old backups +apply_retention() { + log "Applying retention policy..." + + # Daily backups: keep last 7 days + log "Cleaning daily backups (keeping last 7 days)..." + rclone delete "r2:$R2_BUCKET/db/daily/" \ + --min-age 7d \ + -v 2>&1 | grep -v "^$" || true + + # Weekly backups: keep last ~12 weeks (90 days) + log "Cleaning weekly backups (keeping last 12 weeks)..." + rclone delete "r2:$R2_BUCKET/db/weekly/" \ + --min-age 90d \ + -v 2>&1 | grep -v "^$" || true + + # Monthly backups: keep last 12 months (365 days) + log "Cleaning monthly backups (keeping last 12 months)..." + rclone delete "r2:$R2_BUCKET/db/monthly/" \ + --min-age 365d \ + -v 2>&1 | grep -v "^$" || true + + log "Retention policy applied" +} + +# List current backups +list_backups() { + log "Current backups in R2:" + echo "" + echo "=== Daily Backups ===" + rclone ls "r2:$R2_BUCKET/db/daily/" 2>/dev/null || echo "(none)" + echo "" + echo "=== Weekly Backups ===" + rclone ls "r2:$R2_BUCKET/db/weekly/" 2>/dev/null || echo "(none)" + echo "" + echo "=== Monthly Backups ===" + rclone ls "r2:$R2_BUCKET/db/monthly/" 2>/dev/null || echo "(none)" + echo "" + echo "=== Images ===" + local image_count=$(rclone ls "r2:$R2_BUCKET/images/" 2>/dev/null | wc -l) + local image_size=$(rclone size "r2:$R2_BUCKET/images/" 2>/dev/null | grep "Total size" | awk '{print $3, $4}') + echo "Files: $image_count, Size: ${image_size:-0}" +} + +# Main execution +main() { + local action="${1:-backup}" + + case "$action" in + backup) + log "Starting Dogbook R2 backup..." + check_prerequisites + backup_database + sync_images + apply_retention + log "Backup complete!" + ;; + list) + check_prerequisites + list_backups + ;; + *) + echo "Usage: $0 [backup|list]" + echo " backup - Run full backup (default)" + echo " list - List current backups" + exit 1 + ;; + esac +} + +main "$@" diff --git a/deploy/r2-restore.sh b/deploy/r2-restore.sh new file mode 100644 index 0000000..efe472a --- /dev/null +++ b/deploy/r2-restore.sh @@ -0,0 +1,305 @@ +#!/bin/bash +# R2 Restore Script for Dogbook +# Restores database and/or images from Cloudflare R2 backups +# +# Usage: +# ./r2-restore.sh latest # Restore latest database backup +# ./r2-restore.sh db 2024-01-15 # Restore database from specific date +# ./r2-restore.sh images # Restore all images +# ./r2-restore.sh all # Restore both database (latest) and images +# ./r2-restore.sh list # List available backups + +set -euo pipefail + +# Configuration +DATA_DIR="/srv/dogbook/data" +DB_FILE="$DATA_DIR/keystone.db" +IMAGES_DIR="$DATA_DIR/images" +TMP_DIR="/tmp/dogbook-restore" +R2_BUCKET="maisonsdoggo" +SERVICE_NAME="dogbook" + +# Logging +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +error() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2 +} + +confirm() { + local prompt="$1" + read -p "$prompt [y/N] " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] +} + +# Check prerequisites +check_prerequisites() { + if ! command -v rclone &> /dev/null; then + error "rclone is not installed" + exit 1 + fi + + if ! rclone listremotes | grep -q "^r2:$"; then + error "rclone remote 'r2' is not configured" + exit 1 + fi +} + +# Stop the service before restore +stop_service() { + if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then + log "Stopping $SERVICE_NAME service..." + sudo systemctl stop "$SERVICE_NAME" + return 0 + fi + return 1 +} + +# Start the service after restore +start_service() { + log "Starting $SERVICE_NAME service..." + sudo systemctl start "$SERVICE_NAME" +} + +# List available backups +list_backups() { + log "Available backups in R2:" + echo "" + + echo "=== Latest (quick restore) ===" + rclone ls "r2:$R2_BUCKET/latest/" 2>/dev/null || echo "(none)" + echo "" + + echo "=== Daily Backups (last 7 days) ===" + rclone ls "r2:$R2_BUCKET/db/daily/" 2>/dev/null | sort -r || echo "(none)" + echo "" + + echo "=== Weekly Backups (last 12 weeks) ===" + rclone ls "r2:$R2_BUCKET/db/weekly/" 2>/dev/null | sort -r || echo "(none)" + echo "" + + echo "=== Monthly Backups (last 12 months) ===" + rclone ls "r2:$R2_BUCKET/db/monthly/" 2>/dev/null | sort -r || echo "(none)" + echo "" + + echo "=== Images ===" + local image_count=$(rclone ls "r2:$R2_BUCKET/images/" 2>/dev/null | wc -l) + local image_size=$(rclone size "r2:$R2_BUCKET/images/" 2>/dev/null | grep "Total size" | awk '{print $3, $4}') + echo "Files: $image_count, Size: ${image_size:-0}" +} + +# Find a backup file by date +find_backup() { + local date="$1" + local backup_file="" + + # Try daily first + backup_file="db/daily/keystone-$date.db.gz" + if rclone ls "r2:$R2_BUCKET/$backup_file" &>/dev/null; then + echo "$backup_file" + return 0 + fi + + # Try weekly + backup_file="db/weekly/keystone-$date.db.gz" + if rclone ls "r2:$R2_BUCKET/$backup_file" &>/dev/null; then + echo "$backup_file" + return 0 + fi + + # Try monthly + backup_file="db/monthly/keystone-$date.db.gz" + if rclone ls "r2:$R2_BUCKET/$backup_file" &>/dev/null; then + echo "$backup_file" + return 0 + fi + + return 1 +} + +# Restore database +restore_database() { + local source="$1" + local backup_path="" + + mkdir -p "$TMP_DIR" + + if [ "$source" = "latest" ]; then + backup_path="latest/keystone.db.gz" + log "Restoring latest database backup..." + else + # Source is a date (YYYY-MM-DD) + backup_path=$(find_backup "$source") || { + error "No backup found for date: $source" + echo "Available backups:" + list_backups + exit 1 + } + log "Restoring database from: $backup_path" + fi + + # Download the backup + local temp_file="$TMP_DIR/restore.db.gz" + log "Downloading backup..." + rclone copyto "r2:$R2_BUCKET/$backup_path" "$temp_file" + + # Decompress + log "Decompressing..." + gunzip -f "$temp_file" + local restored_db="$TMP_DIR/restore.db" + + # Verify the database integrity + log "Verifying database integrity..." + if ! sqlite3 "$restored_db" "PRAGMA integrity_check;" | grep -q "ok"; then + error "Database integrity check failed!" + rm -f "$restored_db" + exit 1 + fi + + # Create backup of current database + if [ -f "$DB_FILE" ]; then + local current_backup="$DB_FILE.before-restore-$(date +%Y%m%d-%H%M%S)" + log "Backing up current database to: $current_backup" + cp "$DB_FILE" "$current_backup" + fi + + # Stop service, restore, start service + local was_running=false + if stop_service; then + was_running=true + fi + + log "Restoring database..." + mkdir -p "$(dirname "$DB_FILE")" + cp "$restored_db" "$DB_FILE" + + # Set correct ownership (assuming dogbook user) + if id "dogbook" &>/dev/null; then + chown dogbook:dogbook "$DB_FILE" + fi + + if [ "$was_running" = true ]; then + start_service + fi + + # Clean up + rm -f "$restored_db" + + log "Database restored successfully!" + log "Previous database backed up to: ${current_backup:-N/A}" +} + +# Restore images +restore_images() { + log "Restoring images from R2..." + + if ! confirm "This will sync all images from R2. Continue?"; then + log "Aborted." + exit 0 + fi + + mkdir -p "$IMAGES_DIR" + + # Use rclone sync to restore images + rclone sync "r2:$R2_BUCKET/images/" "$IMAGES_DIR" \ + --checksum \ + --transfers 4 \ + --progress + + # Set correct ownership + if id "dogbook" &>/dev/null; then + chown -R dogbook:dogbook "$IMAGES_DIR" + fi + + log "Images restored successfully!" +} + +# Print usage +usage() { + echo "Dogbook R2 Restore Script" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Commands:" + echo " list List available backups" + echo " latest Restore the latest database backup" + echo " db Restore database from a specific date" + echo " images Restore all images from R2" + echo " all Restore both database (latest) and images" + echo "" + echo "Examples:" + echo " $0 list # See what backups are available" + echo " $0 latest # Quick restore from latest backup" + echo " $0 db 2024-01-15 # Restore from January 15, 2024" + echo " $0 images # Restore all images" + echo " $0 all # Full restore (database + images)" + echo "" + echo "Notes:" + echo " - The service will be stopped during database restore" + echo " - Current database is backed up before restoration" + echo " - Images are synced (existing files not deleted unless missing from backup)" +} + +# Main execution +main() { + local command="${1:-}" + local arg="${2:-}" + + case "$command" in + list) + check_prerequisites + list_backups + ;; + latest) + check_prerequisites + if ! confirm "Restore latest database backup?"; then + log "Aborted." + exit 0 + fi + restore_database "latest" + ;; + db) + if [ -z "$arg" ]; then + error "Please specify a date (YYYY-MM-DD)" + usage + exit 1 + fi + check_prerequisites + if ! confirm "Restore database from $arg?"; then + log "Aborted." + exit 0 + fi + restore_database "$arg" + ;; + images) + check_prerequisites + restore_images + ;; + all) + check_prerequisites + if ! confirm "Restore database (latest) and all images?"; then + log "Aborted." + exit 0 + fi + restore_database "latest" + restore_images + ;; + help|--help|-h) + usage + ;; + "") + usage + exit 1 + ;; + *) + error "Unknown command: $command" + usage + exit 1 + ;; + esac +} + +main "$@" diff --git a/deploy/setup-r2-backup.sh b/deploy/setup-r2-backup.sh new file mode 100644 index 0000000..8a021e0 --- /dev/null +++ b/deploy/setup-r2-backup.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# Setup script for Dogbook R2 Backup +# Run this once to configure rclone and install the systemd timer + +set -euo pipefail + +log() { + echo "[SETUP] $1" +} + +error() { + echo "[ERROR] $1" >&2 +} + +# Check if running as root (needed for systemd) +if [ "$EUID" -ne 0 ]; then + error "Please run as root (sudo)" + exit 1 +fi + +DEPLOY_DIR="/srv/dogbook/deploy" +DOGBOOK_USER="dogbook" + +# Install rclone if not present +install_rclone() { + if command -v rclone &> /dev/null; then + log "rclone is already installed: $(rclone version | head -1)" + return 0 + fi + + log "Installing rclone..." + curl -s https://rclone.org/install.sh | bash + log "rclone installed: $(rclone version | head -1)" +} + +# Configure rclone for R2 +configure_rclone() { + local rclone_config="/home/$DOGBOOK_USER/.config/rclone/rclone.conf" + + if [ -f "$rclone_config" ] && grep -q "^\[r2\]$" "$rclone_config"; then + log "rclone R2 remote already configured" + return 0 + fi + + echo "" + echo "=== Cloudflare R2 Configuration ===" + echo "" + echo "You'll need the following from your Cloudflare R2 dashboard:" + echo " 1. Account ID (visible in the R2 dashboard URL)" + echo " 2. Access Key ID (from R2 > Manage API tokens)" + echo " 3. Secret Access Key (from R2 > Manage API tokens)" + echo "" + echo "The endpoint should be: https://.r2.cloudflarestorage.com" + echo "" + + read -p "Enter your R2 Account ID: " account_id + read -p "Enter your R2 Access Key ID: " access_key + read -sp "Enter your R2 Secret Access Key: " secret_key + echo "" + + # Create config directory + mkdir -p "/home/$DOGBOOK_USER/.config/rclone" + + # Create rclone config + cat >> "$rclone_config" << EOF + +[r2] +type = s3 +provider = Cloudflare +access_key_id = $access_key +secret_access_key = $secret_key +endpoint = https://${account_id}.r2.cloudflarestorage.com +acl = private +EOF + + chown -R "$DOGBOOK_USER:$DOGBOOK_USER" "/home/$DOGBOOK_USER/.config" + chmod 600 "$rclone_config" + + log "rclone R2 remote configured" + + # Test the connection + log "Testing R2 connection..." + if sudo -u "$DOGBOOK_USER" rclone lsd r2: &>/dev/null; then + log "R2 connection successful!" + sudo -u "$DOGBOOK_USER" rclone lsd r2: + else + error "R2 connection failed. Please check your credentials." + exit 1 + fi +} + +# Copy scripts to deploy directory +install_scripts() { + log "Installing backup scripts..." + + # Copy scripts if running from repo + if [ -f "$(dirname "$0")/r2-backup.sh" ]; then + cp "$(dirname "$0")/r2-backup.sh" "$DEPLOY_DIR/" + cp "$(dirname "$0")/r2-restore.sh" "$DEPLOY_DIR/" + fi + + chmod +x "$DEPLOY_DIR/r2-backup.sh" + chmod +x "$DEPLOY_DIR/r2-restore.sh" + chown "$DOGBOOK_USER:$DOGBOOK_USER" "$DEPLOY_DIR/r2-backup.sh" + chown "$DOGBOOK_USER:$DOGBOOK_USER" "$DEPLOY_DIR/r2-restore.sh" + + log "Scripts installed to $DEPLOY_DIR" +} + +# Install systemd service and timer +install_systemd() { + log "Installing systemd service and timer..." + + # Copy service files + cp "$DEPLOY_DIR/dogbook-r2-backup.service" /etc/systemd/system/ + cp "$DEPLOY_DIR/dogbook-r2-backup.timer" /etc/systemd/system/ + + # Reload systemd + systemctl daemon-reload + + # Enable and start the timer + systemctl enable dogbook-r2-backup.timer + systemctl start dogbook-r2-backup.timer + + log "Systemd timer installed and started" + echo "" + systemctl status dogbook-r2-backup.timer --no-pager +} + +# Run initial backup +run_initial_backup() { + echo "" + read -p "Run initial backup now? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + log "Running initial backup..." + sudo -u "$DOGBOOK_USER" "$DEPLOY_DIR/r2-backup.sh" backup + log "Initial backup complete!" + fi +} + +# Main +main() { + log "Setting up Dogbook R2 Backup" + echo "" + + install_rclone + configure_rclone + install_scripts + install_systemd + run_initial_backup + + echo "" + log "=== Setup Complete ===" + echo "" + echo "Useful commands:" + echo " # Check backup timer status" + echo " systemctl status dogbook-r2-backup.timer" + echo "" + echo " # Manually trigger a backup" + echo " sudo systemctl start dogbook-r2-backup.service" + echo "" + echo " # View backup logs" + echo " journalctl -u dogbook-r2-backup.service" + echo "" + echo " # List backups" + echo " sudo -u $DOGBOOK_USER $DEPLOY_DIR/r2-backup.sh list" + echo "" + echo " # Restore from backup" + echo " sudo $DEPLOY_DIR/r2-restore.sh latest" + echo "" +} + +main "$@" From bd5278c498103c782acd0ebd22060bf2253cbad9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 14:40:22 +0000 Subject: [PATCH 2/2] Move R2 bucket name to environment variable - Scripts now read R2_BUCKET from /srv/dogbook/data/.env - Setup script prompts for bucket name during configuration - Added R2_BUCKET to .env.production.example --- backend/.env.production.example | 5 +++ deploy/r2-backup.sh | 17 ++++++- deploy/r2-restore.sh | 17 ++++++- deploy/setup-r2-backup.sh | 78 +++++++++++++++++++-------------- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/backend/.env.production.example b/backend/.env.production.example index bf3d785..d96431b 100644 --- a/backend/.env.production.example +++ b/backend/.env.production.example @@ -29,3 +29,8 @@ FRONTEND_URL=https://example.com VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_SUBJECT=mailto:admin@example.com + +# R2 Backup (optional) +# Set this to enable backups to Cloudflare R2 +# Run: sudo ./deploy/setup-r2-backup.sh to configure +R2_BUCKET= diff --git a/deploy/r2-backup.sh b/deploy/r2-backup.sh index 246e632..532cbb4 100644 --- a/deploy/r2-backup.sh +++ b/deploy/r2-backup.sh @@ -11,12 +11,27 @@ set -euo pipefail +# Load environment variables +ENV_FILE="/srv/dogbook/data/.env" +if [ -f "$ENV_FILE" ]; then + set -a + source "$ENV_FILE" + set +a +fi + # Configuration DATA_DIR="/srv/dogbook/data" DB_FILE="$DATA_DIR/keystone.db" IMAGES_DIR="$DATA_DIR/images" TMP_DIR="/tmp/dogbook-backup" -R2_BUCKET="maisonsdoggo" +R2_BUCKET="${R2_BUCKET:-}" + +# Validate required environment variables +if [ -z "$R2_BUCKET" ]; then + echo "ERROR: R2_BUCKET environment variable is not set" >&2 + echo "Add R2_BUCKET=your-bucket-name to $ENV_FILE" >&2 + exit 1 +fi # Date calculations TODAY=$(date +%Y-%m-%d) diff --git a/deploy/r2-restore.sh b/deploy/r2-restore.sh index efe472a..efdfbdf 100644 --- a/deploy/r2-restore.sh +++ b/deploy/r2-restore.sh @@ -11,14 +11,29 @@ set -euo pipefail +# Load environment variables +ENV_FILE="/srv/dogbook/data/.env" +if [ -f "$ENV_FILE" ]; then + set -a + source "$ENV_FILE" + set +a +fi + # Configuration DATA_DIR="/srv/dogbook/data" DB_FILE="$DATA_DIR/keystone.db" IMAGES_DIR="$DATA_DIR/images" TMP_DIR="/tmp/dogbook-restore" -R2_BUCKET="maisonsdoggo" +R2_BUCKET="${R2_BUCKET:-}" SERVICE_NAME="dogbook" +# Validate required environment variables +if [ -z "$R2_BUCKET" ]; then + echo "ERROR: R2_BUCKET environment variable is not set" >&2 + echo "Add R2_BUCKET=your-bucket-name to $ENV_FILE" >&2 + exit 1 +fi + # Logging log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" diff --git a/deploy/setup-r2-backup.sh b/deploy/setup-r2-backup.sh index 8a021e0..c8a70bb 100644 --- a/deploy/setup-r2-backup.sh +++ b/deploy/setup-r2-backup.sh @@ -36,33 +36,32 @@ install_rclone() { # Configure rclone for R2 configure_rclone() { local rclone_config="/home/$DOGBOOK_USER/.config/rclone/rclone.conf" + local env_file="/srv/dogbook/data/.env" if [ -f "$rclone_config" ] && grep -q "^\[r2\]$" "$rclone_config"; then log "rclone R2 remote already configured" - return 0 - fi - - echo "" - echo "=== Cloudflare R2 Configuration ===" - echo "" - echo "You'll need the following from your Cloudflare R2 dashboard:" - echo " 1. Account ID (visible in the R2 dashboard URL)" - echo " 2. Access Key ID (from R2 > Manage API tokens)" - echo " 3. Secret Access Key (from R2 > Manage API tokens)" - echo "" - echo "The endpoint should be: https://.r2.cloudflarestorage.com" - echo "" - - read -p "Enter your R2 Account ID: " account_id - read -p "Enter your R2 Access Key ID: " access_key - read -sp "Enter your R2 Secret Access Key: " secret_key - echo "" - - # Create config directory - mkdir -p "/home/$DOGBOOK_USER/.config/rclone" - - # Create rclone config - cat >> "$rclone_config" << EOF + else + echo "" + echo "=== Cloudflare R2 Configuration ===" + echo "" + echo "You'll need the following from your Cloudflare R2 dashboard:" + echo " 1. Account ID (visible in the R2 dashboard URL)" + echo " 2. Access Key ID (from R2 > Manage API tokens)" + echo " 3. Secret Access Key (from R2 > Manage API tokens)" + echo "" + echo "The endpoint should be: https://.r2.cloudflarestorage.com" + echo "" + + read -p "Enter your R2 Account ID: " account_id + read -p "Enter your R2 Access Key ID: " access_key + read -sp "Enter your R2 Secret Access Key: " secret_key + echo "" + + # Create config directory + mkdir -p "/home/$DOGBOOK_USER/.config/rclone" + + # Create rclone config + cat >> "$rclone_config" << EOF [r2] type = s3 @@ -73,19 +72,30 @@ endpoint = https://${account_id}.r2.cloudflarestorage.com acl = private EOF - chown -R "$DOGBOOK_USER:$DOGBOOK_USER" "/home/$DOGBOOK_USER/.config" - chmod 600 "$rclone_config" + chown -R "$DOGBOOK_USER:$DOGBOOK_USER" "/home/$DOGBOOK_USER/.config" + chmod 600 "$rclone_config" + + log "rclone R2 remote configured" - log "rclone R2 remote configured" + # Test the connection + log "Testing R2 connection..." + if sudo -u "$DOGBOOK_USER" rclone lsd r2: &>/dev/null; then + log "R2 connection successful!" + sudo -u "$DOGBOOK_USER" rclone lsd r2: + else + error "R2 connection failed. Please check your credentials." + exit 1 + fi + fi - # Test the connection - log "Testing R2 connection..." - if sudo -u "$DOGBOOK_USER" rclone lsd r2: &>/dev/null; then - log "R2 connection successful!" - sudo -u "$DOGBOOK_USER" rclone lsd r2: + # Configure bucket name in .env + if grep -q "^R2_BUCKET=" "$env_file" 2>/dev/null; then + log "R2_BUCKET already configured in $env_file" else - error "R2 connection failed. Please check your credentials." - exit 1 + echo "" + read -p "Enter your R2 bucket name: " bucket_name + echo "R2_BUCKET=$bucket_name" >> "$env_file" + log "R2_BUCKET=$bucket_name added to $env_file" fi }