Files
xamxam/docs/nextcloud-sync.md
T

5.7 KiB

Nextcloud off-site backup sync

XAMXAM pushes its latest SQLite snapshot to a Nextcloud folder so that a server/disk failure doesn't take the backup history with it. This is the implementation of Phase 5 — Remote Sync from the archived backup-plan.md.

What it does

Property Value
Destination https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK
Protocol WebDAV (HTTP PUT / PROPFIND / DELETE)
Credentials Reused from smtp_settings (xamxam@erg.be), decrypted via Crypto
Transport PHP curl extension — no rclone, no shell curl
What is uploaded The newest db-*.db.gz snapshot from /var/backups/xamxam
Remote filename xamxam-db-<timestamp>.db.gz (timestamped, not overwritten)
Remote retention Last 7 snapshots (configurable via REMOTE_KEEP)
Cadence Daily, 20 min after the 02:00 snapshot

The uploads are SQLite snapshots only — the CSV and files-ZIP exports (see export.md) are not pushed. This keeps Nextcloud usage minimal; the full export set remains available on-demand from the admin panel.

Sizing

As of writing, the live DB is ~2.5 MB and compresses to ~475 KB. Seven daily snapshots are therefore ~3.3 MB — a negligible fraction of the 10 GB Nextcloud quota for xamxam@erg.be. If more history is desired, raise REMOTE_KEEP in the cron line; there is ample headroom.

Script: scripts/nextcloud-sync.php

CLI script (deployed to /usr/local/bin/nextcloud-sync.php), run by cron.

Flow:

  1. Resolve APP_ROOT (/var/www/xamxam in prod, app/ in dev) and load the composer autoloader.
  2. No-op DatabaseMigrations (read-only — the sync must never mutate the DB).
  3. Load SMTP credentials via SmtpRelay::getSettings() (decrypts the password).
  4. Find the newest db-*.db.gz in /var/backups/xamxam.
  5. PUT it to the WebDAV path (streaming upload, memory-safe).
  6. PROPFIND the uploaded file and verify its size matches the local file (integrity check — a failed sync is detected, not assumed).
  7. PROPFIND the folder, list xamxam-db-*.db.gz, and DELETE the oldest beyond REMOTE_KEEP (7 by default).

Exit code 0 on success, 1 on any failure (logged to /var/log/xamxam-backup-YYYY-MM-DD.log). A sync failure never blocks the local backup — the two jobs are separate cron entries.

Environment variables

Variable Default Meaning
REMOTE_KEEP 7 Number of remote snapshots to retain

Cron wiring

In /etc/cron.d/xamxam-backup (deployed from deploy/xamxam-backup.cron):

# Daily snapshot at 2am — kept 90 days
0  2  *  *  *  www-data  RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1

# Push the latest snapshot to Nextcloud WebDAV (daily, after the 2am snapshot)
20 2  *  *  *  www-data  php /usr/local/bin/nextcloud-sync.php >> /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1

The sync runs as www-data, so it shares the file permissions of the backup script (/var/backups/xamxam is www-data:www-data). The .env holding the crypto key is 640 www-data:xamxam — readable by www-data, so Crypto::decrypt works from the CLI.

Monitoring

The backup watchdog (scripts/backup-watchdog.php) now checks both the local snapshots and the Nextcloud copy:

  • Local — newest db-*.db.gz older than 2 h → alert.
  • Remote — newest xamxam-db-*.db.gz older than 48 h (or none found) → alert.

Both feed the same anti-flood email to xamxam@erg.be (or the configured notify_email). A single alert can report either or both issues.

Manual verification

# Read the backup folder (expect 207 Multi-Status)
curl -sS -u 'xamxam@erg.be:<password>' -X PROPFIND --head \
  'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK' \
  -o /dev/null -w '%{http_code}\n'

# Upload a test file (expect 201), then delete it (expect 204)
curl -sS -u 'xamxam@erg.be:<password>' -X PUT --data-binary test \
  'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK/_write_test.txt' \
  -o /dev/null -w '%{http_code}\n'
curl -sS -u 'xamxam@erg.be:<password>' -X DELETE \
  'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK/_write_test.txt' \
  -o /dev/null -w '%{http_code}\n'

# Run the sync manually (server only)
sudo -u www-data php /usr/local/bin/nextcloud-sync.php

Restoring from Nextcloud

The remote snapshot is the same .db.gz the local backup produces, so restoring follows the standard procedure in deployment.md:

# Download the snapshot
curl -sS -u 'xamxam@erg.be:<password>' -O \
  'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK/xamxam-db-<timestamp>.db.gz'

# Restore
sudo systemctl stop nginx
gunzip -c xamxam-db-<timestamp>.db.gz > /var/www/xamxam/storage/xamxam.db
chown www-data:www-data /var/www/xamxam/storage/xamxam.db
chmod 660 /var/www/xamxam/storage/xamxam.db
sudo systemctl start nginx

Security notes

  • Credentials are read from the DB (smtp_settings) and never logged or echoed; only the resulting success/failure messages are written to the log.
  • The Nextcloud password is the same LDAP/SSO-backed xamxam@erg.be credential used for SMTP — the WebDAV Basic-auth endpoint accepts it directly (verified live: PUT → 201, PROPFIND → 207, DELETE → 204).
  • CURLOPT_SSL_VERIFYPEER / CURLOPT_SSL_VERIFYHOST are enabled (certificate chain is validated).
  • The sync is one-way and additive-then-pruned: it never deletes a snapshot until a newer one has been confirmed uploaded.