#!/usr/bin/env bash # reMarkable Backup (offizielle Cloud via rmapi) # # Ziel: # - Inkrementeller Download in "current/". # - Datierte Snapshots in "snapshots/YYYY-MM-DD_HHMMSS/". # - Versionierung via Hardlinks (unveraenderte Dateien belegen kaum Zusatzplatz). # # Nutzung: # ./backup-official-cloud.sh /www/remarkable/notizen / # set -euo pipefail BASE_DIR="${1:-/www/remarkable/notizen}" REMOTE_DIR="${2:-/}" TIMESTAMP="$(date +%F_%H%M%S)" CURRENT_DIR="${BASE_DIR}/current" SNAPSHOT_ROOT="${BASE_DIR}/snapshots" SNAPSHOT_DIR="${SNAPSHOT_ROOT}/${TIMESTAMP}" LATEST_LINK="${SNAPSHOT_ROOT}/latest" LOG_DIR="${BASE_DIR}/logs" LOCK_FILE="${BASE_DIR}/.backup.lock" # Optional: Pfad, der ein echter Mountpoint sein muss (z. B. /www/remarkable/notizen). # Wenn gesetzt und kein Mountpoint, wird abgebrochen (sicher gegen "auf Root-FS schreiben"). MOUNT_CHECK_PATH="${MOUNT_CHECK_PATH:-}" # Optional: Anzahl alter Snapshots behalten (0 = unbegrenzt). SNAPSHOT_KEEP="${SNAPSHOT_KEEP:-30}" mkdir -p "${CURRENT_DIR}" "${SNAPSHOT_ROOT}" "${LOG_DIR}" if [[ -n "${MOUNT_CHECK_PATH}" ]] && ! mountpoint -q "${MOUNT_CHECK_PATH}"; then echo "Fehler: ${MOUNT_CHECK_PATH} ist nicht gemountet. Backup abgebrochen." exit 1 fi if ! command -v rmapi >/dev/null 2>&1; then echo "Fehler: rmapi nicht im PATH." exit 1 fi if ! command -v rsync >/dev/null 2>&1; then echo "Fehler: rsync fehlt. Bitte installieren." exit 1 fi exec 9>"${LOCK_FILE}" if ! flock -n 9; then echo "Backup laeuft bereits (Lock: ${LOCK_FILE})." exit 1 fi LOG_FILE="${LOG_DIR}/backup-${TIMESTAMP}.log" exec > >(tee -a "${LOG_FILE}") 2>&1 echo "== reMarkable Backup Start ==" echo "Base: ${BASE_DIR}" echo "Remote: ${REMOTE_DIR}" echo "Zeit: ${TIMESTAMP}" MGET_FLAGS=(-i) if [[ "${BACKUP_FULL:-}" == "1" ]]; then MGET_FLAGS=() fi if [[ "${BACKUP_MIRROR_DELETE:-}" == "1" ]]; then MGET_FLAGS+=(-d) fi echo "1) Aktualisiere current/ via rmapi mget ${MGET_FLAGS[*]:-(none)} ..." rmapi mget "${MGET_FLAGS[@]}" -o "${CURRENT_DIR}" "${REMOTE_DIR}" # Leere Ordner anlegen (rmapi mget erstellt keine leeren Verzeichnisse) echo "1b) Lege fehlende Ordner an ..." rmapi find "${REMOTE_DIR}" 2>/dev/null | grep '^\[d\]' | sed 's/^\[d\] *//' | while read -r DIR; do LOCAL_DIR="${CURRENT_DIR}/${DIR}" mkdir -p "${LOCAL_DIR}" done echo "2) Erzeuge Snapshot: ${SNAPSHOT_DIR}" mkdir -p "${SNAPSHOT_DIR}" if [[ -L "${LATEST_LINK}" ]] && [[ -d "$(readlink -f "${LATEST_LINK}")" ]]; then PREV_SNAPSHOT="$(readlink -f "${LATEST_LINK}")" rsync -a --delete --link-dest="${PREV_SNAPSHOT}" "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/" else rsync -a --delete "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/" fi ln -sfn "${SNAPSHOT_DIR}" "${LATEST_LINK}" if [[ "${SNAPSHOT_KEEP}" =~ ^[0-9]+$ ]] && (( SNAPSHOT_KEEP > 0 )); then echo "3) Pruefe Retention (keep=${SNAPSHOT_KEEP}) ..." mapfile -t SNAPSHOTS < <(ls -1 "${SNAPSHOT_ROOT}" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$' | sort) if (( ${#SNAPSHOTS[@]} > SNAPSHOT_KEEP )); then DELETE_COUNT=$(( ${#SNAPSHOTS[@]} - SNAPSHOT_KEEP )) for OLD in "${SNAPSHOTS[@]:0:DELETE_COUNT}"; do rm -rf "${SNAPSHOT_ROOT}/${OLD}" echo " entfernt: ${SNAPSHOT_ROOT}/${OLD}" done fi fi # PDF-Konvertierung if command -v remarkable-convert.sh >/dev/null 2>&1; then echo "4) Konvertiere .rmdoc zu PDF ..." remarkable-convert.sh "${CURRENT_DIR}" fi echo "== Backup fertig ==" echo "Current: ${CURRENT_DIR}" echo "Snapshot: ${SNAPSHOT_DIR}"