mirror of
https://github.com/IBICO74/nextcloud-backup.git
synced 2026-07-03 02:57:05 +00:00
423 lines
13 KiB
Bash
Executable file
423 lines
13 KiB
Bash
Executable file
#!/bin/bash
|
|
#
|
|
# Nextcloud Backup Script v2.0 - NO SUDO REQUIRED
|
|
# Runs as: backup-user (member of www-data and docker groups)
|
|
#
|
|
# Backup strategy:
|
|
# - Daily: Metadata (database + config + docker files)
|
|
# - Weekly (Monday): Full sync of data
|
|
# - Daily (Tuesday-Sunday): Incremental copy of changed files
|
|
# - Yearly (January 1st): Full sync to yearly/
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# ============================================================================
|
|
# CONFIGURATION
|
|
# ============================================================================
|
|
|
|
# Paths
|
|
NEXTCLOUD_DOCKER_ROOT="/mnt/data/docker-stacks/nextcloud-stack"
|
|
NEXTCLOUD_DATA_ROOT="/mnt/data/docker/volumes/nextcloud-stack_nextcloud-data/_data"
|
|
BACKUP_ROOT="/opt/backup"
|
|
LOCAL_BACKUP_DIR="$BACKUP_ROOT/backups"
|
|
LOG_DIR="$BACKUP_ROOT/logs"
|
|
RCLONE_CONFIG="/opt/backup/.config/rclone/rclone.conf"
|
|
|
|
# Docker containers
|
|
DB_CONTAINER="nextcloud-db"
|
|
APP_CONTAINER="nextcloud-app"
|
|
|
|
# Rclone remote (change to your remote name)
|
|
RCLONE_REMOTE="jottacloud:ServerBackup/Nextcloud"
|
|
|
|
# Retention (number of backups to keep)
|
|
DAILY_RETENTION=10
|
|
WEEKLY_RETENTION=10
|
|
YEARLY_RETENTION=10
|
|
|
|
# ============================================================================
|
|
# LOGGING
|
|
# ============================================================================
|
|
|
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
LOG_FILE="$LOG_DIR/backup-$TIMESTAMP.log"
|
|
|
|
# Ensure log directory exists
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
log() {
|
|
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
log_error() {
|
|
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] ERROR: $*" | tee -a "$LOG_FILE" >&2
|
|
}
|
|
|
|
# ============================================================================
|
|
# ERROR HANDLING
|
|
# ============================================================================
|
|
|
|
cleanup() {
|
|
local exit_code=$?
|
|
if [ $exit_code -ne 0 ]; then
|
|
log_error "Backup failed with exit code: $exit_code"
|
|
else
|
|
log "✅ Backup completed successfully"
|
|
fi
|
|
exit $exit_code
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
# ============================================================================
|
|
# ENVIRONMENT VALIDATION
|
|
# ============================================================================
|
|
|
|
log "========================================="
|
|
log "🚀 Starting Nextcloud Backup v2.0"
|
|
log "========================================="
|
|
log "User: $(whoami)"
|
|
log "Groups: $(groups)"
|
|
log "Timestamp: $TIMESTAMP"
|
|
log ""
|
|
|
|
# Check required commands
|
|
for cmd in docker rclone tar gzip; do
|
|
if ! command -v $cmd &> /dev/null; then
|
|
log_error "Required command not found: $cmd"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Verify we're running as backup-user
|
|
if [ "$(whoami)" != "backup-user" ]; then
|
|
log_error "This script must run as backup-user"
|
|
log_error "Current user: $(whoami)"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify group membership
|
|
if ! groups | grep -q www-data; then
|
|
log_error "backup-user is not in www-data group"
|
|
exit 1
|
|
fi
|
|
|
|
if ! groups | grep -q docker; then
|
|
log_error "backup-user is not in docker group"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify rclone config exists
|
|
if [ ! -f "$RCLONE_CONFIG" ]; then
|
|
log_error "rclone config not found: $RCLONE_CONFIG"
|
|
log_error "Run: sudo -u backup-user rclone config"
|
|
exit 1
|
|
fi
|
|
|
|
# Test rclone connection
|
|
log "Testing rclone connection..."
|
|
if ! rclone about "$RCLONE_REMOTE" --config "$RCLONE_CONFIG" &> /dev/null; then
|
|
log_error "rclone connection test failed"
|
|
exit 1
|
|
fi
|
|
log "✅ rclone connection OK"
|
|
|
|
# ============================================================================
|
|
# DETERMINE BACKUP TYPE
|
|
# ============================================================================
|
|
|
|
CURRENT_DATE=$(date +%Y-%m-%d)
|
|
DAY_OF_WEEK=$(date +%u) # 1 = Monday, 7 = Sunday
|
|
MONTH_DAY=$(date +%m-%d)
|
|
WEEK_NUMBER=$(date +%Y-W%V)
|
|
YEAR=$(date +%Y)
|
|
|
|
# Determine if this is a weekly backup day (Monday)
|
|
IS_WEEKLY_BACKUP=false
|
|
if [ "$DAY_OF_WEEK" = "1" ]; then
|
|
IS_WEEKLY_BACKUP=true
|
|
log "📅 Weekly backup day (Monday)"
|
|
fi
|
|
|
|
# Determine if this is yearly backup day (January 1st)
|
|
IS_YEARLY_BACKUP=false
|
|
if [ "$MONTH_DAY" = "01-01" ]; then
|
|
IS_YEARLY_BACKUP=true
|
|
log "📅 Yearly backup day (January 1st)"
|
|
fi
|
|
|
|
# ============================================================================
|
|
# METADATA BACKUP (DAILY)
|
|
# ============================================================================
|
|
|
|
log ""
|
|
log "========================================="
|
|
log "📦 METADATA BACKUP"
|
|
log "========================================="
|
|
|
|
DAILY_BACKUP_PATH="$RCLONE_REMOTE/daily/$TIMESTAMP"
|
|
METADATA_DIR="$LOCAL_BACKUP_DIR/metadata-$TIMESTAMP"
|
|
mkdir -p "$METADATA_DIR"
|
|
|
|
# 1. Database backup
|
|
log "Backing up database..."
|
|
DB_FILE="$METADATA_DIR/database.sql"
|
|
if docker exec "$DB_CONTAINER" mysqldump \
|
|
-u root \
|
|
-p"$(grep MYSQL_ROOT_PASSWORD "$NEXTCLOUD_DOCKER_ROOT/.env" | cut -d= -f2)" \
|
|
--single-transaction \
|
|
--quick \
|
|
--lock-tables=false \
|
|
nextcloud > "$DB_FILE" 2>> "$LOG_FILE"; then
|
|
|
|
gzip "$DB_FILE"
|
|
DB_SIZE=$(du -h "$DB_FILE.gz" | cut -f1)
|
|
log "✅ Database backup: $DB_SIZE"
|
|
else
|
|
log_error "Database backup failed"
|
|
exit 1
|
|
fi
|
|
|
|
# 2. Config backup
|
|
log "Backing up config..."
|
|
CONFIG_TAR="$METADATA_DIR/config.tar.gz"
|
|
if tar -czf "$CONFIG_TAR" -C "$NEXTCLOUD_DATA_ROOT" config/ 2>> "$LOG_FILE"; then
|
|
CONFIG_SIZE=$(du -h "$CONFIG_TAR" | cut -f1)
|
|
log "✅ Config backup: $CONFIG_SIZE"
|
|
else
|
|
log_error "Config backup failed"
|
|
exit 1
|
|
fi
|
|
|
|
# 3. Docker stack files backup
|
|
log "Backing up Docker stack files..."
|
|
DOCKER_TAR="$METADATA_DIR/docker-stack.tar.gz"
|
|
if tar -czf "$DOCKER_TAR" \
|
|
-C "$NEXTCLOUD_DOCKER_ROOT" \
|
|
docker-compose.yml \
|
|
.env 2>> "$LOG_FILE"; then
|
|
|
|
DOCKER_SIZE=$(du -h "$DOCKER_TAR" | cut -f1)
|
|
log "✅ Docker stack backup: $DOCKER_SIZE"
|
|
else
|
|
log_error "Docker stack backup failed"
|
|
exit 1
|
|
fi
|
|
|
|
# 4. Upload metadata to Jottacloud
|
|
log "Uploading metadata to Jottacloud..."
|
|
if rclone copy "$METADATA_DIR" "$DAILY_BACKUP_PATH/metadata/" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" \
|
|
--log-level INFO \
|
|
--stats 1m \
|
|
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
log "✅ Metadata uploaded to $DAILY_BACKUP_PATH"
|
|
else
|
|
log_error "Metadata upload failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Cleanup local metadata backup
|
|
rm -rf "$METADATA_DIR"
|
|
log "🧹 Cleaned up local metadata files"
|
|
|
|
# ============================================================================
|
|
# DATA BACKUP (WEEKLY + INCREMENTAL)
|
|
# ============================================================================
|
|
|
|
log ""
|
|
log "========================================="
|
|
log "💾 DATA BACKUP"
|
|
log "========================================="
|
|
|
|
DATA_SOURCE="$NEXTCLOUD_DATA_ROOT/data/"
|
|
|
|
# Check if data directory exists and is readable
|
|
if [ ! -d "$DATA_SOURCE" ]; then
|
|
log_error "Data directory not found: $DATA_SOURCE"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -r "$DATA_SOURCE" ]; then
|
|
log_error "Data directory not readable: $DATA_SOURCE"
|
|
log_error "Verify that backup-user is in www-data group"
|
|
exit 1
|
|
fi
|
|
|
|
DATA_SIZE=$(du -sh "$DATA_SOURCE" 2>/dev/null | cut -f1 || echo "unknown")
|
|
log "Data directory size: $DATA_SIZE"
|
|
|
|
# Weekly backup: Full sync (Monday)
|
|
if [ "$IS_WEEKLY_BACKUP" = true ]; then
|
|
log "🔄 Weekly full sync to: weekly/$WEEK_NUMBER/data/"
|
|
WEEKLY_DEST="$RCLONE_REMOTE/weekly/$WEEK_NUMBER/data/"
|
|
|
|
if rclone sync "$DATA_SOURCE" "$WEEKLY_DEST" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" \
|
|
--log-level INFO \
|
|
--stats 5m \
|
|
--transfers 4 \
|
|
--checkers 8 \
|
|
--exclude '.Trash-*/**' \
|
|
--exclude '**/.~*' \
|
|
--exclude '**/~$*' \
|
|
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
log "✅ Weekly data sync completed"
|
|
else
|
|
log_error "Weekly data sync failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Also copy metadata to weekly folder
|
|
log "Copying metadata to weekly folder..."
|
|
rclone copy "$DAILY_BACKUP_PATH/metadata/" "$RCLONE_REMOTE/weekly/$WEEK_NUMBER/metadata/" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" 2>&1 | tee -a "$LOG_FILE"
|
|
|
|
else
|
|
# Daily incremental: Copy only new/changed files (Tuesday-Sunday)
|
|
log "📋 Daily incremental copy to: weekly/$WEEK_NUMBER/data/"
|
|
WEEKLY_DEST="$RCLONE_REMOTE/weekly/$WEEK_NUMBER/data/"
|
|
|
|
if rclone copy "$DATA_SOURCE" "$WEEKLY_DEST" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" \
|
|
--log-level INFO \
|
|
--stats 5m \
|
|
--update \
|
|
--transfers 4 \
|
|
--checkers 8 \
|
|
--exclude '.Trash-*/**' \
|
|
--exclude '**/.~*' \
|
|
--exclude '**/~$*' \
|
|
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
log "✅ Daily incremental copy completed"
|
|
else
|
|
log_error "Daily incremental copy failed"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ============================================================================
|
|
# YEARLY BACKUP (January 1st)
|
|
# ============================================================================
|
|
|
|
if [ "$IS_YEARLY_BACKUP" = true ]; then
|
|
log ""
|
|
log "========================================="
|
|
log "🗓️ YEARLY BACKUP"
|
|
log "========================================="
|
|
|
|
YEARLY_DEST="$RCLONE_REMOTE/yearly/$YEAR/data/"
|
|
|
|
log "🔄 Yearly full sync to: yearly/$YEAR/data/"
|
|
if rclone sync "$DATA_SOURCE" "$YEARLY_DEST" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" \
|
|
--log-level INFO \
|
|
--stats 5m \
|
|
--transfers 4 \
|
|
--checkers 8 \
|
|
--exclude '.Trash-*/**' \
|
|
--exclude '**/.~*' \
|
|
--exclude '**/~$*' \
|
|
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
log "✅ Yearly data sync completed"
|
|
else
|
|
log_error "Yearly data sync failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Copy metadata to yearly folder
|
|
log "Copying metadata to yearly folder..."
|
|
rclone copy "$DAILY_BACKUP_PATH/metadata/" "$RCLONE_REMOTE/yearly/$YEAR/metadata/" \
|
|
--config "$RCLONE_CONFIG" \
|
|
--log-file "$LOG_FILE" 2>&1 | tee -a "$LOG_FILE"
|
|
fi
|
|
|
|
# ============================================================================
|
|
# ROTATION (CLEANUP OLD BACKUPS)
|
|
# ============================================================================
|
|
|
|
log ""
|
|
log "========================================="
|
|
log "🧹 ROTATION"
|
|
log "========================================="
|
|
|
|
# Daily rotation
|
|
log "Rotating daily backups (keeping $DAILY_RETENTION)..."
|
|
DAILY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/daily/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
|
DAILY_COUNT=$(echo "$DAILY_BACKUPS" | grep -c . || echo 0)
|
|
|
|
if [ "$DAILY_COUNT" -gt "$DAILY_RETENTION" ]; then
|
|
DAILY_TO_DELETE=$(echo "$DAILY_BACKUPS" | tail -n +$((DAILY_RETENTION + 1)))
|
|
echo "$DAILY_TO_DELETE" | while read -r dir; do
|
|
if [ -n "$dir" ]; then
|
|
log " Deleting old daily backup: $dir"
|
|
rclone purge "$RCLONE_REMOTE/daily/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
|
fi
|
|
done
|
|
fi
|
|
log "✅ Daily retention: $DAILY_COUNT backups"
|
|
|
|
# Weekly rotation
|
|
log "Rotating weekly backups (keeping $WEEKLY_RETENTION)..."
|
|
WEEKLY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/weekly/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
|
WEEKLY_COUNT=$(echo "$WEEKLY_BACKUPS" | grep -c . || echo 0)
|
|
|
|
if [ "$WEEKLY_COUNT" -gt "$WEEKLY_RETENTION" ]; then
|
|
WEEKLY_TO_DELETE=$(echo "$WEEKLY_BACKUPS" | tail -n +$((WEEKLY_RETENTION + 1)))
|
|
echo "$WEEKLY_TO_DELETE" | while read -r dir; do
|
|
if [ -n "$dir" ]; then
|
|
log " Deleting old weekly backup: $dir"
|
|
rclone purge "$RCLONE_REMOTE/weekly/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
|
fi
|
|
done
|
|
fi
|
|
log "✅ Weekly retention: $WEEKLY_COUNT backups"
|
|
|
|
# Yearly rotation
|
|
log "Rotating yearly backups (keeping $YEARLY_RETENTION)..."
|
|
YEARLY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/yearly/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
|
YEARLY_COUNT=$(echo "$YEARLY_BACKUPS" | grep -c . || echo 0)
|
|
|
|
if [ "$YEARLY_COUNT" -gt "$YEARLY_RETENTION" ]; then
|
|
YEARLY_TO_DELETE=$(echo "$YEARLY_BACKUPS" | tail -n +$((YEARLY_RETENTION + 1)))
|
|
echo "$YEARLY_TO_DELETE" | while read -r dir; do
|
|
if [ -n "$dir" ]; then
|
|
log " Deleting old yearly backup: $dir"
|
|
rclone purge "$RCLONE_REMOTE/yearly/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
|
fi
|
|
done
|
|
fi
|
|
log "✅ Yearly retention: $YEARLY_COUNT backups"
|
|
|
|
# ============================================================================
|
|
# SUMMARY
|
|
# ============================================================================
|
|
|
|
log ""
|
|
log "========================================="
|
|
log "📊 BACKUP SUMMARY"
|
|
log "========================================="
|
|
log "Date: $CURRENT_DATE"
|
|
log "Type: $([ "$IS_YEARLY_BACKUP" = true ] && echo "Yearly + Weekly + Daily" || ([ "$IS_WEEKLY_BACKUP" = true ] && echo "Weekly + Daily" || echo "Daily"))"
|
|
log "Duration: $SECONDS seconds"
|
|
log "Log file: $LOG_FILE"
|
|
log ""
|
|
|
|
# Storage usage
|
|
log "Jottacloud storage:"
|
|
rclone about "$RCLONE_REMOTE" --config "$RCLONE_CONFIG" 2>/dev/null | head -3 | tee -a "$LOG_FILE" || log "Could not retrieve storage info"
|
|
|
|
log ""
|
|
log "========================================="
|
|
log "✅ BACKUP COMPLETED SUCCESSFULLY"
|
|
log "========================================="
|