Initial commit: Nextcloud backup system with rclone

Automated backup solution for Dockerized Nextcloud installations.

Features:
- Secure backup-user approach (no sudo required)
- Hybrid backup strategy (daily metadata + weekly/yearly full)
- rclone support for 40+ cloud providers
- Configurable retention policies
- Docker-aware with maintenance mode support

Includes:
- nextcloud-backup.sh: Main backup script (v2, recommended)
- nextcloud-backup-v1.sh: Legacy sudo version (reference only)
- README.md: Complete documentation
- nextcloud-backup.cron: Cron schedule example
This commit is contained in:
Kenneth Augdal 2025-10-11 19:48:15 +02:00
commit 19f6a04b51
5 changed files with 848 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Sensitive files
*.env
*.conf
rclone.conf
*.log
*.txt
secrets/
passwords.txt
*.key
*.pem
.env.*
# Backup files
*.tar.gz
*.sql
*.sql.gz
backups/
logs/
# System files
.DS_Store
Thumbs.db
*~
.*.swp

156
README.md Normal file
View file

@ -0,0 +1,156 @@
# Nextcloud Backup System
Automated Nextcloud backup solution using rclone to cloud storage (Jottacloud, Dropbox, Google Drive, etc).
## Features
- 🔒 **Secure**: No sudo required, runs as dedicated backup user
- 📦 **Hybrid Strategy**: Daily metadata backups + weekly/yearly full backups
- 🔄 **Automated**: Runs via cron
- ☁️ **Cloud Storage**: Uses rclone (supports 40+ cloud providers)
- 🗜️ **Compression**: Automatic tar.gz compression
- 📊 **Retention**: Configurable retention policies
- 🐳 **Docker Support**: Works with Dockerized Nextcloud
## Quick Start
### Prerequisites
- Ubuntu/Debian Linux server
- Docker-based Nextcloud installation
- rclone installed and configured
- Dedicated backup user with proper group permissions
### Installation
1. **Create backup user**:
```bash
sudo adduser --system --group --home /opt/backup backup-user
sudo usermod -aG www-data,docker backup-user
```
2. **Create directory structure**:
```bash
sudo mkdir -p /opt/backup/{bin,logs,backups}
sudo chown -R backup-user:backup-user /opt/backup
```
3. **Configure rclone** (as backup-user):
```bash
sudo -u backup-user rclone config
```
4. **Install backup script**:
```bash
sudo cp nextcloud-backup.sh /opt/backup/bin/
sudo chmod +x /opt/backup/bin/nextcloud-backup.sh
```
5. **Edit configuration** in script:
```bash
RCLONE_REMOTE="your-remote:YourPath/Nextcloud"
```
6. **Set read-only permissions** on Nextcloud data:
```bash
sudo chmod 750 /path/to/nextcloud/data
```
7. **Test backup**:
```bash
sudo -u backup-user /opt/backup/bin/nextcloud-backup.sh
```
8. **Schedule with cron**:
```bash
sudo cp nextcloud-backup.cron /etc/cron.d/nextcloud-backup
```
## Configuration
Edit these variables in `nextcloud-backup.sh`:
```bash
# Rclone remote (change to your remote name)
RCLONE_REMOTE="jottacloud:ServerBackup/Nextcloud"
# Docker container name
NEXTCLOUD_CONTAINER="nextcloud-app"
# Retention policy
DAILY_RETENTION=10 # Keep 10 daily backups
WEEKLY_RETENTION=10 # Keep 10 weekly backups
YEARLY_RETENTION=10 # Keep 10 yearly backups
```
## Backup Strategy
- **Daily** (03:15): Metadata only (database, config, apps) - ~100-200MB
- **Weekly** (Sunday): Complete backup including user data - several GB
- **Yearly** (January 1st): Full archive for long-term storage
## Security Best Practices
**Recommended**: Use dedicated backup user (no sudo)
- Read-only access to Nextcloud data
- Limited permissions
- Group-based access (www-data, docker)
**Not recommended**: Using sudo
- Excessive privileges
- Security risk
- Audit trail issues
See `nextcloud-backup-v1.sh` for legacy sudo-based approach (not recommended for new installations).
## Troubleshooting
### Permission Denied
```bash
# Verify backup-user groups
id backup-user
# Should show: www-data, docker
# Check data directory permissions
ls -ld /path/to/nextcloud/data
# Should be: drwxr-x--- (750)
```
### Rclone Authentication Failed
```bash
# Reconfigure as backup-user
sudo -u backup-user rclone config reconnect your-remote:
```
### Docker Command Not Found
```bash
# Add backup-user to docker group
sudo usermod -aG docker backup-user
```
## Files
- `nextcloud-backup.sh` - Main backup script (v2, no sudo)
- `nextcloud-backup-v1.sh` - Legacy version with sudo (reference only)
- `nextcloud-backup.cron` - Cron schedule example
- `README.md` - This file
## Requirements
- **rclone** - Cloud storage sync tool
- **Docker** - For Nextcloud containers
- **tar** - Archive creation
- **gzip** - Compression
## License
MIT License - Feel free to use and modify
## Author
Created by IBICO74
## Support
For issues or questions, please open a GitHub issue.

237
nextcloud-backup-v1.sh Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_ROOT="/mnt/data/backups/nextcloud-daily"
REMOTE_BASE="jottacloud:ServerBackup/Nextcloud"
ENV_FILE="/mnt/data/docker-stacks/nextcloud-stack/.env"
DATA_ROOT="/mnt/data/docker/volumes/nextcloud-stack_nextcloud-data/_data"
RCLONE_CONFIG="/home/kau005/.config/rclone/rclone.conf"
DB_CONTAINER="nextcloud-db"
APP_CONTAINER="nextcloud-app"
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
WORKDIR="${BACKUP_ROOT}/${TIMESTAMP}"
LOG_DIR="/var/log/nextcloud-backup"
LOG_FILE="${LOG_DIR}/${TIMESTAMP}.log"
RETENTION_DAYS=10
RETENTION_WEEKS=10
RETENTION_YEARS=10
MAINTENANCE_SET=0
sudo chown kau005:kau005 "$WORKDIR" "$LOG_DIR" 2>/dev/null || true
sudo chown kau005:kau005 "$WORKDIR" "$LOG_DIR" 2>/dev/null || true
mkdir -p "$WORKDIR" "$LOG_DIR"
log() {
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" | tee -a "$LOG_FILE"
}
require_file() {
local path="$1"
if [[ ! -f "$path" ]]; then
log "ERROR: Finner ikke nødvendig fil: $path"
exit 1
fi
}
require_dir() {
local path="$1"
if ! sudo test -d "$path"; then
log "ERROR: Finner ikke nødvendig katalog: $path"
exit 1
fi
}
cleanup() {
if [[ $MAINTENANCE_SET -eq 1 ]]; then
log "Deaktiverer maintenance mode"
if ! docker exec -u www-data "$APP_CONTAINER" php occ maintenance:mode --off >>"$LOG_FILE" 2>&1; then
log "ADVARSEL: klarte ikke å slå av maintenance mode"
else
MAINTENANCE_SET=0
fi
fi
}
trap cleanup EXIT
require_file "$ENV_FILE"
require_dir "$DATA_ROOT"
require_file "$RCLONE_CONFIG"
MYSQL_USER="$(awk -F= '/^MYSQL_USER=/{print $2; exit}' "$ENV_FILE")"
MYSQL_PASSWORD="$(awk -F= '/^MYSQL_PASSWORD=/{print $2; exit}' "$ENV_FILE")"
MYSQL_DATABASE="$(awk -F= '/^MYSQL_DATABASE=/{print $2; exit}' "$ENV_FILE")"
: "${MYSQL_USER:=nextcloud}"
: "${MYSQL_PASSWORD:?Fant ikke MYSQL_PASSWORD i $ENV_FILE}"
: "${MYSQL_DATABASE:=nextcloud}"
export RCLONE_CONFIG
log "Starter Nextcloud-backup ${TIMESTAMP}"
if ! docker ps --format '{{.Names}}' | grep -qx "$APP_CONTAINER"; then
log "ERROR: Finner ikke applikasjonscontaineren '$APP_CONTAINER'"
exit 1
fi
log "Aktiverer maintenance mode"
if docker exec -u www-data "$APP_CONTAINER" php occ maintenance:mode --on >>"$LOG_FILE" 2>&1; then
MAINTENANCE_SET=1
else
log "ERROR: Klarte ikke å slå på maintenance mode"
exit 1
fi
DB_DUMP="${WORKDIR}/nextcloud-db-${TIMESTAMP}.sql.gz"
log " > Tar database-dump til ${DB_DUMP}"
if ! docker ps --format '{{.Names}}' | grep -qx "$DB_CONTAINER"; then
log "ERROR: Finner ikke databasecontaineren '$DB_CONTAINER'"
exit 1
fi
if ! docker exec "$DB_CONTAINER" bash -c "MYSQL_PWD='$MYSQL_PASSWORD' mysqldump --single-transaction --quick --lock-tables=false -u '$MYSQL_USER' '$MYSQL_DATABASE'" | gzip >"$DB_DUMP"; then
log "ERROR: Klarte ikke å hente database-dump"
exit 1
fi
CONFIG_ARCHIVE="${WORKDIR}/nextcloud-config-${TIMESTAMP}.tar.gz"
log " > Arkiverer konfigurasjon til ${CONFIG_ARCHIVE}"
(
cd "$DATA_ROOT"
tar -czf "$CONFIG_ARCHIVE" config custom_apps themes 2>/dev/null || true
)
STACK_DIR="/mnt/data/docker-stacks/nextcloud-stack"
if [[ -d "$STACK_DIR" ]]; then
log " > Kopierer docker-stack-filer"
cp -p "$STACK_DIR"/.env "$WORKDIR/stack.env"
cp -p "$STACK_DIR"/docker-compose.yml "$WORKDIR/docker-compose.yml" || true
fi
echo "database_dump=${DB_DUMP##*/}" >"${WORKDIR}/manifest.txt"
echo "config_archive=${CONFIG_ARCHIVE##*/}" >>"${WORKDIR}/manifest.txt"
log "Laster opp metadata til Jottacloud"
if ! rclone copy "$WORKDIR" "${REMOTE_BASE}/daily/${TIMESTAMP}" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
log "ERROR: rclone copy for metadata feilet"
exit 1
fi
# Ukentlig data-backup (synkroniseres hver dag, ny uke starter på mandag)
DAY_OF_WEEK=$(date +%u)
WEEK_STAMP="$(date +%Y-W%V)" # Norsk ukenummer (ISO 8601)
WEEKLY_REMOTE="${REMOTE_BASE}/weekly/${WEEK_STAMP}"
if [[ "$DAY_OF_WEEK" == "1" ]]; then
# Mandag: Start ny uke med full sync (ny referanse)
log "=== UKENTLIG BACKUP - NY UKE (full sync) ==="
log " > Laster opp metadata til ${WEEK_STAMP}"
if ! rclone copy "$WORKDIR" "${WEEKLY_REMOTE}/metadata/" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
log "ADVARSEL: Metadata upload til ukentlig backup feilet"
fi
log " > Starter full sync av data-mappe til ${WEEK_STAMP}/data/ (dette tar tid)..."
if sudo rclone sync "${DATA_ROOT}/data" "${WEEKLY_REMOTE}/data/" \
--config "$RCLONE_CONFIG" \
--progress \
--transfers 4 \
--checkers 8 \
--log-level INFO \
--stats 2m >>"$LOG_FILE" 2>&1; then
log " > Full data-sync fullført for uke ${WEEK_STAMP}"
else
log "ADVARSEL: Data-sync feilet"
fi
else
# Tirsdag-søndag: Inkrementell oppdatering (kun nye/endrede filer)
log "=== DAGLIG DATA-BACKUP (inkrementell) ==="
log " > Oppdaterer data for uke ${WEEK_STAMP} (kun endringer)..."
if sudo rclone copy "${DATA_ROOT}/data" "${WEEKLY_REMOTE}/data/" \
--config "$RCLONE_CONFIG" \
--update \
--transfers 4 \
--checkers 8 \
--log-level INFO \
--stats 1m >>"$LOG_FILE" 2>&1; then
log " > Inkrementell data-backup fullført"
else
log "ADVARSEL: Inkrementell data-backup feilet"
fi
fi
# Årlig backup (1. januar) - FULL DATA SNAPSHOT
MONTH_DAY=$(date +%m-%d)
if [[ "$MONTH_DAY" == "01-01" ]]; then
log "=== ÅRLIG BACKUP (full snapshot) ==="
YEAR_STAMP="$(date +%Y)"
YEARLY_REMOTE="${REMOTE_BASE}/yearly/${YEAR_STAMP}"
log " > Laster opp metadata til ${YEAR_STAMP}"
if ! rclone copy "$WORKDIR" "${YEARLY_REMOTE}/metadata/" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
log "ADVARSEL: Metadata upload til årlig backup feilet"
fi
log " > Starter full sync av data-mappe til ${YEAR_STAMP}/data/ (dette tar tid)..."
if sudo rclone sync "${DATA_ROOT}/data" "${YEARLY_REMOTE}/data/" \
--config "$RCLONE_CONFIG" \
--progress \
--transfers 4 \
--checkers 8 \
--log-level INFO \
--stats 2m >>"$LOG_FILE" 2>&1; then
log " > Årlig data-snapshot fullført for ${YEAR_STAMP}"
else
log "ADVARSEL: Årlig data-snapshot feilet"
fi
fi
log "Roterer backups i Jottacloud"
# Daglige backups (behold 10 nyeste)
DAILY_DIRS=$(rclone lsf "${REMOTE_BASE}/daily/" --dirs-only --max-age 365d 2>>"$LOG_FILE" | sort -r || true)
if [[ -n "$DAILY_DIRS" ]]; then
DAILY_COUNT=$(echo "$DAILY_DIRS" | wc -l)
if [[ $DAILY_COUNT -gt $RETENTION_DAYS ]]; then
log " > Sletter $((DAILY_COUNT - RETENTION_DAYS)) gamle daglige backups"
echo "$DAILY_DIRS" | tail -n +$((RETENTION_DAYS + 1)) | while read -r dir; do
if [[ -n "$dir" ]]; then
log " - Sletter daily/${dir}"
rclone purge "${REMOTE_BASE}/daily/${dir}" >>"$LOG_FILE" 2>&1 || true
fi
done
fi
fi
# Ukentlige backups (behold 10 nyeste)
WEEKLY_DIRS=$(rclone lsf "${REMOTE_BASE}/weekly/" --dirs-only --max-age 3650d 2>>"$LOG_FILE" | sort -r || true)
if [[ -n "$WEEKLY_DIRS" ]]; then
WEEKLY_COUNT=$(echo "$WEEKLY_DIRS" | wc -l)
if [[ $WEEKLY_COUNT -gt $RETENTION_WEEKS ]]; then
log " > Sletter $((WEEKLY_COUNT - RETENTION_WEEKS)) gamle ukentlige backups"
echo "$WEEKLY_DIRS" | tail -n +$((RETENTION_WEEKS + 1)) | while read -r dir; do
if [[ -n "$dir" ]]; then
log " - Sletter weekly/${dir}"
rclone purge "${REMOTE_BASE}/weekly/${dir}" >>"$LOG_FILE" 2>&1 || true
fi
done
fi
fi
# Årlige backups (behold 10 nyeste)
YEARLY_DIRS=$(rclone lsf "${REMOTE_BASE}/yearly/" --dirs-only 2>>"$LOG_FILE" | sort -r || true)
if [[ -n "$YEARLY_DIRS" ]]; then
YEARLY_COUNT=$(echo "$YEARLY_DIRS" | wc -l)
if [[ $YEARLY_COUNT -gt $RETENTION_YEARS ]]; then
log " > Sletter $((YEARLY_COUNT - RETENTION_YEARS)) gamle årlige backups"
echo "$YEARLY_DIRS" | tail -n +$((RETENTION_YEARS + 1)) | while read -r dir; do
if [[ -n "$dir" ]]; then
log " - Sletter yearly/${dir}"
rclone purge "${REMOTE_BASE}/yearly/${dir}" >>"$LOG_FILE" 2>&1 || true
fi
done
fi
fi
log "Opprydning av lokale staging-kataloger eldre enn ${RETENTION_DAYS} dager"
sudo find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" -exec rm -rf {} + || true
log "Backup fullført"

8
nextcloud-backup.cron Normal file
View file

@ -0,0 +1,8 @@
# Nextcloud Backup - Cron Schedule
# Runs daily at 03:15 as backup-user
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Daily backup at 03:15
15 3 * * * backup-user /opt/backup/bin/nextcloud-backup.sh >> /opt/backup/logs/cron.log 2>&1

423
nextcloud-backup.sh Executable file
View file

@ -0,0 +1,423 @@
#!/bin/bash
#
# Nextcloud Backup Script v2.0 - UTEN SUDO
# Kjører som: backup-user (medlem av www-data og docker grupper)
#
# Backup-strategi:
# - Daglig: Metadata (database + config + docker-filer)
# - Ukentlig (mandag): Full sync av data
# - Daglig (tirsdag-søndag): Inkrementell copy av endrede filer
# - Årlig (1. januar): Full sync til yearly/
#
set -euo pipefail
# ============================================================================
# KONFIGURASJON
# ============================================================================
# Stier
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
RCLONE_REMOTE="jottacloudbackup:ServerBackup/Nextcloud"
# Retention (antall backups å beholde)
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 "========================================="