mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
The docs already tell users that pushing the wiki to a remote git repository
is a supported backup pattern: docs/deploy.md#backups says 'markdown - back
up with rsync or git push to a remote', docs/design-decisions.md says 'No
remote/cloud sync (use git remote on the wiki dir)', and both
docs/airgapped-install.md and SECURITY.md defer the channel security to the
operator ('Remote sync security'). But the recipe itself is nowhere. A
homelab or laptop install has no built-in remote-push cadence, and building
one from scratch takes 100+ lines of bash plus systemd units plus a
gitignore that catches derived state and secrets.
Fill the gap with a docs-only addition:
- New docs/backup.md walks through what to include (wiki/, raw/, config.toml
minus secrets) vs exclude (db/ derived from wiki via reindex, models/
redownloadable, logs/, .serve.lock), the rsync + git push + tarball flow,
the systemd --user timer schedule, restore, and the SECURITY.md-aligned
posture (private repo, least-privilege push credential, secret exclusion,
encryption at rest is out of scope for ai-memory).
- A worked example under docs/examples/backup/ following the precedent set
by docs/examples/jev-reranker-adapter/ and docs/examples/auto-improve-eval/:
the snapshot script (configurable through six env vars), a systemd --user
.service oneshot, a daily .timer, a .gitignore for the mirror repo, and
a README with the install-and-enable steps plus non-systemd equivalents
for macOS launchd, Windows Task Scheduler via WSL, and Docker sidecar.
- One-line pointers from docs/deploy.md#backups (right after the tarball
routine) and docs/airgapped-install.md (extending the existing 'git
remote sync' bullet).
- A row in the README docs table between lifecycle-ops.md and
llm-providers.md, positioned as a companion to lifecycle-ops.md.
The on-box "ai-memory backup --to TARBALL" command is unchanged. This is
docs-only; no core CLI subcommand for remote push is proposed here (the
issue leaves that decision to the maintainer).
Refs #950.
Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
4.5 KiB
Bash
118 lines
4.5 KiB
Bash
#!/usr/bin/env bash
|
|
# ai-memory-snapshot.sh
|
|
#
|
|
# Daily snapshot of an ai-memory single-user install:
|
|
# 1. rsync the mutable data dir into a git-tracked mirror checkout,
|
|
# excluding derived state and secrets (see .gitignore in this dir).
|
|
# 2. commit + push the mirror to a remote git repository when anything
|
|
# changed.
|
|
# 3. write a compressed tarball of the same tree to a second location
|
|
# (a mounted external disk, a network share, or on WSL2 the Windows-side
|
|
# filesystem) so the SQLite index can be recovered from disk even if the
|
|
# remote repository is unreachable.
|
|
#
|
|
# Assumptions:
|
|
# - You already ran `git init && git remote add origin <url>` inside
|
|
# $REPO_DIR and staged the .gitignore shipped alongside this file.
|
|
# - The user running this script has an ssh-agent or credential helper set
|
|
# up so `git push` is non-interactive.
|
|
# - $DATA_SRC is the ai-memory data dir (`ai-memory status` prints it) and
|
|
# $CFG_SRC is where your provider env-file lives.
|
|
#
|
|
# Configure these four variables for your host, or export them from the
|
|
# systemd unit's Environment= lines. Sample values are commented out.
|
|
: "${REPO_DIR:=$HOME/ai-memory-backup}" # local mirror checkout, `git init`'d and pointed at a remote
|
|
: "${DATA_SRC:=$HOME/.local/share/ai-memory}" # ai-memory data dir
|
|
: "${CFG_SRC:=$HOME/.config/ai-memory}" # ai-memory config dir (may hold provider env files)
|
|
: "${TARBALL_DIR:=$HOME/backups/ai-memory}" # second-location tarball drop (external disk, network share, ...)
|
|
: "${LOG_DIR:=$HOME/logs/ai-memory-snapshot}" # rotated per run
|
|
: "${TARBALL_RETENTION_DAYS:=30}" # how long to keep tarballs
|
|
: "${LOG_RETENTION_DAYS:=30}" # how long to keep run logs
|
|
: "${SECRET_EXCLUDES:=*.env auth.json .secrets}" # glob patterns for files never to sync
|
|
|
|
set -euo pipefail
|
|
|
|
stamp=$(date +%Y-%m-%d)
|
|
log="$LOG_DIR/ai-memory-snapshot_$(date +%Y%m%d-%H%M%S).log"
|
|
mkdir -p "$LOG_DIR" "$TARBALL_DIR"
|
|
exec >"$log" 2>&1
|
|
|
|
echo "=== $(date -Iseconds) ai-memory snapshot start ==="
|
|
echo "data_src=$DATA_SRC"
|
|
echo "cfg_src=$CFG_SRC"
|
|
echo "repo_dir=$REPO_DIR"
|
|
echo "tarball_dir=$TARBALL_DIR"
|
|
|
|
if [ ! -d "$DATA_SRC" ]; then
|
|
echo "ERROR: $DATA_SRC does not exist. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
if [ ! -d "$REPO_DIR/.git" ]; then
|
|
echo "ERROR: $REPO_DIR is not a git repository (run 'git init' and 'git remote add origin <url>' first)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Build a comma-free rsync exclude list. The .gitignore in the mirror repo
|
|
# also keeps derived state out of commits, but excluding here keeps the rsync
|
|
# working set small.
|
|
rsync_excludes=(
|
|
--exclude=.git/
|
|
--exclude=models/ # embedder weights, redownloadable
|
|
--exclude=db/ # SQLite index, derived from wiki via `ai-memory reindex`
|
|
--exclude=logs/ # rotated tracing output
|
|
--exclude=.serve.lock # server pidfile
|
|
)
|
|
for glob in $SECRET_EXCLUDES; do
|
|
rsync_excludes+=(--exclude="$glob")
|
|
done
|
|
|
|
echo "--- rsync data ---"
|
|
rsync -a --delete "${rsync_excludes[@]}" "$DATA_SRC/" "$REPO_DIR/data/"
|
|
|
|
echo "--- rsync config (secrets excluded) ---"
|
|
config_excludes=()
|
|
for glob in $SECRET_EXCLUDES; do
|
|
config_excludes+=(--exclude="$glob")
|
|
done
|
|
rsync -a --delete "${config_excludes[@]}" "$CFG_SRC/" "$REPO_DIR/config/"
|
|
|
|
cd "$REPO_DIR"
|
|
git add -A
|
|
|
|
if git diff --cached --quiet; then
|
|
echo "no changes since last snapshot; skipping commit + push"
|
|
else
|
|
git commit -m "auto: snapshot $stamp" | tail -3
|
|
echo "--- push ---"
|
|
timeout 180 git push origin "$(git rev-parse --abbrev-ref HEAD)" 2>&1 | tail -10
|
|
fi
|
|
|
|
echo "--- tarball to second location ---"
|
|
tarball="$TARBALL_DIR/ai-memory-$stamp.tar.gz"
|
|
tar_excludes=(
|
|
--exclude=.git
|
|
--exclude=models
|
|
--exclude=db
|
|
--exclude=logs
|
|
--exclude=.serve.lock
|
|
)
|
|
for glob in $SECRET_EXCLUDES; do
|
|
tar_excludes+=(--exclude="$glob")
|
|
done
|
|
# Use paths relative to $HOME so the archive is portable across usernames.
|
|
tar czf "$tarball" "${tar_excludes[@]}" \
|
|
-C "$HOME" \
|
|
"${DATA_SRC#$HOME/}" \
|
|
"${CFG_SRC#$HOME/}"
|
|
echo "tarball: $(ls -lh "$tarball" | awk '{print $5, $9}')"
|
|
|
|
echo "--- rotate tarballs (retention: ${TARBALL_RETENTION_DAYS}d) ---"
|
|
find "$TARBALL_DIR" -maxdepth 1 -name 'ai-memory-*.tar.gz' -type f \
|
|
-mtime "+${TARBALL_RETENTION_DAYS}" -delete -print
|
|
|
|
echo "--- rotate logs (retention: ${LOG_RETENTION_DAYS}d) ---"
|
|
find "$LOG_DIR" -maxdepth 1 -name 'ai-memory-snapshot_*.log' -type f \
|
|
-mtime "+${LOG_RETENTION_DAYS}" -delete -print
|
|
|
|
echo "=== $(date -Iseconds) ai-memory snapshot done ==="
|