#!/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_REMOTE_RSYNC rsync destination for hard-link-deduplicated # off-host copy, e.g. backup@rsync.net:librenotes/ # Layout written at the target: # /YYYY-MM-DD/librenotes-.tar.gz # Each run passes --link-dest=..// so # unchanged archives cost zero extra bytes. # BACKUP_REMOTE_SSH_KEY path to a private SSH key used for the rsync # leg (passed via -e "ssh -i "). # 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, rsync, 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 if [ -n "${BACKUP_REMOTE_RSYNC:-}" ]; then # Compute today / previous-day directory names. Layout at the # remote target is /YYYY-MM-DD/. We pass --link-dest pointing # at yesterday's dir so unchanged archives become hard links — # ~free for daily snapshots that are mostly identical. today="$(date -u +%Y-%m-%d)" yesterday="$(date -u -d 'yesterday' +%Y-%m-%d 2>/dev/null \ || date -u -v-1d +%Y-%m-%d)" ssh_opts=() if [ -n "${BACKUP_REMOTE_SSH_KEY:-}" ]; then ssh_opts=(-e "ssh -i $BACKUP_REMOTE_SSH_KEY -o StrictHostKeyChecking=accept-new") fi # Strip any trailing slash so we control the join. remote="${BACKUP_REMOTE_RSYNC%/}" rsync -a --link-dest="../$yesterday/" "${ssh_opts[@]}" \ "$archive" "$remote/$today/" echo "rsync'd $archive -> $remote/$today/" fi