#!/usr/bin/env bash # backup.sh — daily backup of librenotes state. # # Produces $BACKUP_DIR/librenotes-YYYYMMDD-HHMMSS.tar.gz containing: # - librenotes.db (consistent SQLite .backup snapshot) # - notes.tar.gz (per-tenant note files) # - info.txt (timestamp, hostname, version) # # Required env: # LIBRENOTES_DB path to the SQLite database # LIBRENOTES_DATA_DIR path to the per-tenant note directory # # Optional env: # BACKUP_DIR where to write archives (default /var/backups/librenotes) # BACKUP_REMOTE rclone target for off-site copy (e.g. s3:bucket/path) # BACKUP_VERSION version string written into info.txt # # The script is intentionally a single self-contained file so it # can run on a minimal host with only sqlite3, tar, gzip, and # (optionally) rclone. set -euo pipefail : "${LIBRENOTES_DB:?LIBRENOTES_DB is required}" : "${LIBRENOTES_DATA_DIR:?LIBRENOTES_DATA_DIR is required}" BACKUP_DIR="${BACKUP_DIR:-/var/backups/librenotes}" BACKUP_VERSION="${BACKUP_VERSION:-unknown}" mkdir -p "$BACKUP_DIR" ts="$(date -u +%Y%m%d-%H%M%S)" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT # Online SQLite snapshot. .backup is atomic from the application's # perspective even while writes are happening. sqlite3 "$LIBRENOTES_DB" ".backup '$work/librenotes.db'" # Notes archive. Use --warning=no-file-changed because per-user # files may be touched concurrently; we still get a consistent # point-in-time view per file thanks to tar's read semantics. tar --warning=no-file-changed -C "$LIBRENOTES_DATA_DIR" -czf "$work/notes.tar.gz" . cat > "$work/info.txt" </dev/null || stat -f%z "$work/librenotes.db") notes_size:$(stat -c%s "$work/notes.tar.gz" 2>/dev/null || stat -f%z "$work/notes.tar.gz") EOF archive="$BACKUP_DIR/librenotes-$ts.tar.gz" tar -C "$work" -czf "$archive" librenotes.db notes.tar.gz info.txt echo "wrote $archive ($(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive") bytes)" if [ -n "${BACKUP_REMOTE:-}" ]; then rclone copy "$archive" "$BACKUP_REMOTE" --quiet echo "uploaded to $BACKUP_REMOTE" fi