diff --git a/docs/superpowers/plans/2026-07-23-nas-backup.md b/docs/superpowers/plans/2026-07-23-nas-backup.md new file mode 100644 index 0000000..3dfe398 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-nas-backup.md @@ -0,0 +1,555 @@ +# NAS Backup for Postgres + TDLib State Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `backup` container to the DragonsStash stack that takes daily, encrypted, deduplicated backups of the Postgres database and both TDLib state volumes, and ships them to a Synology NAS over NFS. + +**Architecture:** A small Alpine-based image (restic + postgresql16-client + curl + dcron) runs as its own compose service. A crontab fires `backup.sh` daily at 03:00, which dumps Postgres, tars the TDLib volumes, hands both to `restic backup` against an NFS-backed Docker volume, prunes with `restic forget --keep-daily 14`, and reports success/failure to an Uptime Kuma push monitor. Matches the existing `worker`/`bot` pattern: build context in the repo's `docker-compose.yml`, prebuilt image in `/opt/stacks/DragonsStash/docker-compose.yml`, built and pushed by `.drone.yml`. + +**Tech Stack:** Alpine 3.20, restic 0.16, postgresql16-client, dcron, bash, Docker Compose NFS volume driver. + +## Global Constraints + +- Retention: `restic forget --keep-daily 14 --prune` (14-day window, per approved spec). +- Schedule: daily backup at 03:00, weekly `restic check` at 04:00 Sunday. +- No Docker socket mount, no `privileged: true` — the backup container must not be able to control sibling containers. +- No host-level mount — NFS access only via Docker's native `driver_opts: type: nfs` volume, never `/etc/fstab`. +- Encryption and retention are restic's job — no hand-rolled `age`/`gpg`/`find -mtime` logic. +- TDLib volumes are tarred live (best-effort) — never pause `worker`/`bot` for the backup. +- Restore is a manual, documented procedure only — never scripted/automated. + +**Required user input before Task 5 can run:** `NAS_HOST` and `NAS_EXPORT_PATH` (the Synology NFS share details) and a Kuma Push-monitor URL (`KUMA_PUSH_URL`, created manually in the existing Uptime Kuma instance, ~26h expected heartbeat interval). Tasks 1–4 need none of these and can proceed immediately; do not substitute placeholder values for them in Task 5 — stop and ask the user instead. + +--- + +### Task 1: Backup image (Dockerfile + entrypoint) + +**Files:** +- Create: `backup/Dockerfile` +- Create: `backup/entrypoint.sh` +- Create: `backup/crontab` + +**Interfaces:** +- Produces: a buildable image tagged `dragonsstash-backup:test` locally, with `/entrypoint.sh` as `ENTRYPOINT`, `/backup.sh` present at the image root (written in Task 2 — this task only needs the `COPY` line and a placeholder-free stub isn't acceptable, so create an empty‑body-but-real `backup/backup.sh` here containing just `#!/bin/bash` + `exit 0`, and Task 2 replaces its contents), `restic`, `pg_dump`/`pg_restore`, `curl`, `tar`, `bash`, `dcron` all on `PATH`. +- Consumes: nothing from earlier tasks. + +- [ ] **Step 1: Write `backup/backup.sh` stub** + +```bash +#!/bin/bash +set -euo pipefail +exit 0 +``` + +- [ ] **Step 2: Write `backup/entrypoint.sh`** + +```bash +#!/bin/bash +set -euo pipefail + +if ! restic snapshots >/dev/null 2>&1; then + restic init +fi + +exec crond -f -l 2 +``` + +- [ ] **Step 3: Write `backup/crontab`** + +``` +0 3 * * * /backup.sh >> /proc/1/fd/1 2>&1 +0 4 * * 0 restic check >> /proc/1/fd/1 2>&1 +``` + +- [ ] **Step 4: Write `backup/Dockerfile`** + +```dockerfile +FROM alpine:3.20 + +RUN apk add --no-cache restic postgresql16-client curl tzdata dcron tar bash + +COPY backup/backup.sh /backup.sh +COPY backup/entrypoint.sh /entrypoint.sh +COPY backup/crontab /etc/crontabs/root + +RUN chmod +x /backup.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] +``` + +- [ ] **Step 5: Build the image** + +Run: `cd /home/sam/Documents/DragonsStash && docker build -t dragonsstash-backup:test -f backup/Dockerfile .` +Expected: build completes with `Successfully tagged dragonsstash-backup:test` (or Buildkit's equivalent final `naming to docker.io/library/dragonsstash-backup:test done`), no errors. + +- [ ] **Step 6: Verify the tools are present** + +Run: `docker run --rm dragonsstash-backup:test restic version && docker run --rm dragonsstash-backup:test pg_dump --version` +Expected: `restic 0.16.x ...` and `pg_dump (PostgreSQL) 16.x` printed, both commands exit 0. + +- [ ] **Step 7: Verify the entrypoint initializes an empty repo and starts cron** + +```bash +mkdir -p /tmp/backup-repo-smoke +docker run -d --name backup-smoke \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=smoketest \ + -v /tmp/backup-repo-smoke:/backups \ + dragonsstash-backup:test +sleep 2 +docker logs backup-smoke +docker exec backup-smoke restic snapshots +docker rm -f backup-smoke +rm -rf /tmp/backup-repo-smoke +``` + +Expected: `docker logs` shows no errors (restic init ran silently); `restic snapshots` prints an empty snapshot list (repo exists, header row only, no error). + +- [ ] **Step 8: Commit** + +```bash +git add backup/Dockerfile backup/entrypoint.sh backup/backup.sh backup/crontab +git commit -m "Add backup service image (Dockerfile, entrypoint, crontab)" +``` + +--- + +### Task 2: `backup.sh` script + +**Files:** +- Modify: `backup/backup.sh` (replace Task 1's stub with the real script) + +**Interfaces:** +- Consumes: the image built in Task 1 (`dragonsstash-backup:test`), rebuilt after this change. +- Produces: `/backup.sh`, invoked by cron in Task 1's `crontab` and manually in Task 6's verification. Reads env vars `POSTGRES_USER`, `PGPASSWORD`, `POSTGRES_DB`, `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, `KUMA_PUSH_URL`. Assumes network hostname `dragonsstash-db:5432` for Postgres and mounts `/data/tdlib-worker`, `/data/tdlib-bot` (read-only) for TDLib state. + +- [ ] **Step 1: Replace `backup/backup.sh` with the real script** + +```bash +#!/bin/bash +set -euo pipefail + +report_failure() { + curl -fsS "$KUMA_PUSH_URL" --get \ + --data-urlencode "status=down" \ + --data-urlencode "msg=$BASH_COMMAND failed" || true +} +trap report_failure ERR + +DUMP_FILE=/tmp/dragonsstash.dump +TAR_FILE=/tmp/tdlib.tar.gz + +pg_dump -h dragonsstash-db -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc -f "$DUMP_FILE" + +tar czf "$TAR_FILE" -C /data tdlib-worker tdlib-bot + +restic backup "$DUMP_FILE" "$TAR_FILE" +restic forget --keep-daily 14 --prune + +rm -f "$DUMP_FILE" "$TAR_FILE" + +curl -fsS "$KUMA_PUSH_URL" --get \ + --data-urlencode "status=up" \ + --data-urlencode "msg=OK" +``` + +- [ ] **Step 2: Rebuild the image** + +Run: `docker build -t dragonsstash-backup:test -f backup/Dockerfile .` +Expected: build succeeds. + +- [ ] **Step 3: Stand up a scratch Postgres aliased as `dragonsstash-db`** + +```bash +docker network create backup-test-net 2>/dev/null || true +docker run -d --name test-pg --network backup-test-net --network-alias dragonsstash-db \ + -e POSTGRES_USER=dragons -e POSTGRES_PASSWORD=stash -e POSTGRES_DB=dragonsstash \ + postgres:16-alpine +sleep 5 +docker exec test-pg pg_isready -U dragons -d dragonsstash +``` + +Expected: `accepting connections`. + +- [ ] **Step 4: Stand up a mock Kuma push endpoint** + +```bash +mkdir -p /tmp/mock-kuma-root && touch /tmp/mock-kuma-root/push +docker run -d --name mock-kuma --network backup-test-net \ + -v /tmp/mock-kuma-root:/srv -w /srv python:3-alpine \ + python3 -m http.server 8000 +sleep 1 +``` + +- [ ] **Step 5: Prepare fake TDLib state and a local restic repo dir** + +```bash +mkdir -p /tmp/backup-test/tdlib-worker /tmp/backup-test/tdlib-bot /tmp/backup-test/repo +echo "fake-session" > /tmp/backup-test/tdlib-worker/state.bin +echo "fake-session" > /tmp/backup-test/tdlib-bot/state.bin +``` + +- [ ] **Step 6: Initialize the test restic repo and run `backup.sh` (success path)** + +```bash +docker run --rm --network backup-test-net \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=testpassword \ + -v /tmp/backup-test/repo:/backups \ + --entrypoint restic dragonsstash-backup:test init + +docker run --rm --network backup-test-net \ + -e POSTGRES_USER=dragons -e POSTGRES_PASSWORD=stash -e PGPASSWORD=stash -e POSTGRES_DB=dragonsstash \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=testpassword \ + -e KUMA_PUSH_URL=http://mock-kuma:8000/push \ + -v /tmp/backup-test/tdlib-worker:/data/tdlib-worker:ro \ + -v /tmp/backup-test/tdlib-bot:/data/tdlib-bot:ro \ + -v /tmp/backup-test/repo:/backups \ + --entrypoint /backup.sh dragonsstash-backup:test +echo "exit code: $?" +``` + +Expected: exit code `0`, restic prints a line like `snapshot xxxxxxxx saved`, no error output. + +- [ ] **Step 7: Verify the snapshot landed and contains both files** + +```bash +docker run --rm -v /tmp/backup-test/repo:/backups \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=testpassword \ + --entrypoint restic dragonsstash-backup:test snapshots + +docker run --rm -v /tmp/backup-test/repo:/backups \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=testpassword \ + --entrypoint restic dragonsstash-backup:test ls latest +``` + +Expected: `snapshots` shows exactly one entry; `ls latest` lists `/tmp/dragonsstash.dump` and `/tmp/tdlib.tar.gz`. + +- [ ] **Step 8: Verify the failure path reports to Kuma** + +```bash +docker run --rm --network backup-test-net \ + -e POSTGRES_USER=dragons -e POSTGRES_PASSWORD=wrongpass -e PGPASSWORD=wrongpass -e POSTGRES_DB=dragonsstash \ + -e RESTIC_REPOSITORY=/backups/restic-repo -e RESTIC_PASSWORD=testpassword \ + -e KUMA_PUSH_URL=http://mock-kuma:8000/push \ + -v /tmp/backup-test/tdlib-worker:/data/tdlib-worker:ro \ + -v /tmp/backup-test/tdlib-bot:/data/tdlib-bot:ro \ + -v /tmp/backup-test/repo:/backups \ + --entrypoint /backup.sh dragonsstash-backup:test +echo "exit code: $?" +docker logs mock-kuma | tail -5 +``` + +Expected: exit code nonzero (pg_dump auth failure trips `set -e`); `docker logs mock-kuma` shows a GET request line containing `status=down`. + +- [ ] **Step 9: Clean up test resources** + +```bash +docker rm -f test-pg mock-kuma +docker network rm backup-test-net +rm -rf /tmp/backup-test /tmp/mock-kuma-root +``` + +- [ ] **Step 10: Commit** + +```bash +git add backup/backup.sh +git commit -m "Implement backup.sh: pg_dump + tdlib tar + restic backup/forget + Kuma reporting" +``` + +--- + +### Task 3: Wire the `backup` service into the repo's `docker-compose.yml` + +**Files:** +- Modify: `docker-compose.yml` +- Modify: `.env.example` + +**Interfaces:** +- Consumes: `backup/Dockerfile` (Task 1), `backup/backup.sh` (Task 2). +- Produces: a `backup` compose service buildable via `docker compose build backup`, and a `nas_backups` named volume other tasks (5) will mirror into the production compose file. + +- [ ] **Step 1: Add the `backup` service to `docker-compose.yml`** + +Insert after the existing `bot` service (before `db`): + +```yaml + backup: + build: + context: . + dockerfile: backup/Dockerfile + pull_policy: never + environment: + - POSTGRES_USER=${POSTGRES_USER:-dragons} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-stash} + - PGPASSWORD=${POSTGRES_PASSWORD:-stash} + - POSTGRES_DB=${POSTGRES_DB:-dragonsstash} + - RESTIC_REPOSITORY=/backups/restic-repo + - RESTIC_PASSWORD=${RESTIC_PASSWORD:?Set RESTIC_PASSWORD in .env} + - KUMA_PUSH_URL=${KUMA_PUSH_URL:?Set KUMA_PUSH_URL in .env} + - TZ=${TZ:-Etc/UTC} + volumes: + - tdlib_state:/data/tdlib-worker:ro + - tdlib_bot_state:/data/tdlib-bot:ro + - nas_backups:/backups + depends_on: + db: + condition: service_healthy + restart: unless-stopped + deploy: + resources: + limits: + memory: 256M + networks: + - backend +``` + +- [ ] **Step 2: Add the `nas_backups` volume to the `volumes:` block** + +```yaml + nas_backups: + driver_opts: + type: nfs + o: "addr=${NAS_HOST},rw,nfsvers=4,soft,timeo=100" + device: ":${NAS_EXPORT_PATH}" +``` + +- [ ] **Step 3: Document the new env vars in `.env.example`** + +Append: + +``` +# Backup (NAS via NFS + restic) +NAS_HOST="" # Synology NAS IP or hostname reachable from this host +NAS_EXPORT_PATH="" # NFS export path, e.g. /volume1/dragonsstash-backups +RESTIC_PASSWORD="" # generate with: openssl rand -base64 32 +KUMA_PUSH_URL="" # Uptime Kuma Push monitor URL (create the monitor first) +TZ="Etc/UTC" +``` + +- [ ] **Step 4: Validate the compose file parses** + +Run (with dummy values so the `:?` guards don't fail parsing — `AUTH_SECRET` is required by the existing `app` service, not by this change, but `config` validates the whole file): +```bash +RESTIC_PASSWORD=dummy KUMA_PUSH_URL=http://dummy NAS_HOST=dummy NAS_EXPORT_PATH=/dummy AUTH_SECRET=dummy \ + docker compose -f docker-compose.yml config --quiet +``` +Expected: no output, exit code 0 (a syntax/interpolation error would print to stderr and exit nonzero). + +- [ ] **Step 5: Validate the service actually builds through Compose** + +Run: +```bash +RESTIC_PASSWORD=dummy KUMA_PUSH_URL=http://dummy NAS_HOST=dummy NAS_EXPORT_PATH=/dummy AUTH_SECRET=dummy \ + docker compose -f docker-compose.yml build backup +``` +Expected: build succeeds (reuses Task 1's image layers). + +- [ ] **Step 6: Commit** + +```bash +git add docker-compose.yml .env.example +git commit -m "Add backup service and nas_backups volume to docker-compose.yml" +``` + +--- + +### Task 4: CI — build and push the backup image + +**Files:** +- Modify: `.drone.yml` + +**Interfaces:** +- Consumes: `backup/Dockerfile` (Task 1). +- Produces: `git.samagsteribbe.nl/admin/dragonsstash-backup:latest` (and `:`), pushed on every push to `main`. Task 5's production compose file references this image tag. + +- [ ] **Step 1: Add a `build-backup` step, mirroring `build-worker`/`build-bot`** + +Insert after the existing `build-bot` step in `.drone.yml`: + +```yaml + - name: build-backup + image: plugins/docker + depends_on: [clone] + settings: + repo: git.samagsteribbe.nl/admin/dragonsstash-backup + registry: git.samagsteribbe.nl + dockerfile: backup/Dockerfile + tags: + - latest + - "${DRONE_COMMIT_SHA:0:8}" + username: + from_secret: gitea_username + password: + from_secret: gitea_password +``` + +- [ ] **Step 2: Add `build-backup` to the `deploy` step's `depends_on`** + +Change: +```yaml + - name: deploy + image: alpine + depends_on: [build-app, build-worker, build-bot] +``` +to: +```yaml + - name: deploy + image: alpine + depends_on: [build-app, build-worker, build-bot, build-backup] +``` + +- [ ] **Step 3: Validate YAML syntax** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.drone.yml')); print('OK')"` +Expected: `OK` printed, no exception. + +- [ ] **Step 4: Commit** + +```bash +git add .drone.yml +git commit -m "Add build-backup CI step, include it in deploy dependencies" +``` + +--- + +### Task 5: Wire production (`/opt/stacks/DragonsStash`) — requires NAS + Kuma details from the user + +**Do not start this task until the user has supplied `NAS_HOST` and `NAS_EXPORT_PATH` for the Synology NFS share, and has created an Uptime Kuma Push monitor (name it `dragonsstash-backup`, ~26h expected heartbeat interval) and shared its push URL. If any of these are missing, stop and ask — do not substitute placeholder values here, since this file drives the real deployment.** + +**Files:** +- Modify: `/opt/stacks/DragonsStash/docker-compose.yml` +- Modify: `/opt/stacks/DragonsStash/.env` + +**Interfaces:** +- Consumes: `git.samagsteribbe.nl/admin/dragonsstash-backup:latest` (published by Task 4's CI step once merged/pushed), the real `NAS_HOST`/`NAS_EXPORT_PATH`/`KUMA_PUSH_URL` values gathered above. +- Produces: a running `dragonsstash-backup` container on the production host, verified in Task 6. + +- [ ] **Step 1: Generate `RESTIC_PASSWORD` and add all four new vars to `/opt/stacks/DragonsStash/.env`** + +```bash +cd /opt/stacks/DragonsStash +printf '\n# Backup (NAS via NFS + restic)\nNAS_HOST=""\nNAS_EXPORT_PATH=""\nRESTIC_PASSWORD="%s"\nKUMA_PUSH_URL=""\nTZ="Etc/UTC"\n' "$(openssl rand -base64 32)" >> .env +``` + +Replace the two `` placeholders with the real NAS details and Kuma push URL before saving — this step cannot be completed with the literal placeholder text left in place. + +- [ ] **Step 2: Add the `backup` service to `/opt/stacks/DragonsStash/docker-compose.yml`** + +Insert after the existing `bot` service (before `db`): + +```yaml + backup: + image: git.samagsteribbe.nl/admin/dragonsstash-backup:latest + container_name: dragonsstash-backup + restart: unless-stopped + environment: + - POSTGRES_USER=${POSTGRES_USER:-dragons} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-stash} + - PGPASSWORD=${POSTGRES_PASSWORD:-stash} + - POSTGRES_DB=${POSTGRES_DB:-dragonsstash} + - RESTIC_REPOSITORY=/backups/restic-repo + - RESTIC_PASSWORD=${RESTIC_PASSWORD:?Set RESTIC_PASSWORD in .env} + - KUMA_PUSH_URL=${KUMA_PUSH_URL:?Set KUMA_PUSH_URL in .env} + - TZ=${TZ:-Etc/UTC} + volumes: + - tdlib_state:/data/tdlib-worker:ro + - tdlib_bot_state:/data/tdlib-bot:ro + - nas_backups:/backups + depends_on: + db: + condition: service_healthy + deploy: + resources: + limits: + memory: 256M + networks: + - internal +``` + +- [ ] **Step 3: Add the `nas_backups` volume** + +```yaml + nas_backups: + driver_opts: + type: nfs + o: "addr=${NAS_HOST},rw,nfsvers=4,soft,timeo=100" + device: ":${NAS_EXPORT_PATH}" +``` + +- [ ] **Step 4: Validate the production compose file parses with the real `.env`** + +Run: `cd /opt/stacks/DragonsStash && docker compose config --quiet` +Expected: no output, exit code 0. + +- [ ] **Step 5: Commit is not applicable here** — `/opt/stacks/DragonsStash` is a deployed copy, not the git repo (confirm with `git -C /opt/stacks/DragonsStash status` — expect "not a git repository"). Skip committing; Task 6 deploys these file changes directly. + +--- + +### Task 6: Deploy and verify + +**Files:** none (operational task) + +**Interfaces:** +- Consumes: everything from Tasks 1–5. +- Produces: a running, verified backup on the real NAS, and one completed restore drill. + +- [ ] **Step 1: Confirm with the user before pushing/deploying** + +Pushing to `main` triggers Drone CI to build all four images and deploy to the production host via SSH (`docker compose pull && docker compose up -d`). Confirm the user wants this to happen now before proceeding — this affects the live stack. + +- [ ] **Step 2: Push to `main`** + +```bash +cd /home/sam/Documents/DragonsStash +git push origin main +``` + +Expected: Drone pipeline runs `build-app`, `build-worker`, `build-bot`, `build-backup`, then `deploy`, all green. Check via the Drone UI or `drone build info admin/DragonsStash ` if the `drone` CLI is configured. + +- [ ] **Step 3: Confirm the container is up on the production host** + +```bash +ssh sam@192.168.68.68 "docker ps --filter name=dragonsstash-backup --format '{{.Names}}\t{{.Status}}'" +``` + +Expected: `dragonsstash-backup Up ...`. + +- [ ] **Step 4: Trigger one manual backup run and confirm a snapshot lands on the NAS** + +```bash +ssh sam@192.168.68.68 "docker exec dragonsstash-backup /backup.sh" +ssh sam@192.168.68.68 "docker exec dragonsstash-backup restic snapshots" +``` + +Expected: `backup.sh` exits 0; `restic snapshots` lists exactly one entry. + +- [ ] **Step 5: Confirm the Uptime Kuma push monitor shows green** + +Open the Uptime Kuma dashboard and check the `dragonsstash-backup` monitor's status is up with a recent heartbeat. + +- [ ] **Step 6: Restore drill — prove the backup is actually restorable** + +```bash +ssh sam@192.168.68.68 "docker exec dragonsstash-backup restic restore latest --target /tmp/restore-drill" +ssh sam@192.168.68.68 "docker exec dragonsstash-backup ls -la /tmp/restore-drill/tmp" +``` + +Expected: `/tmp/restore-drill/tmp/dragonsstash.dump` and `/tmp/restore-drill/tmp/tdlib.tar.gz` both present with nonzero size. Then, on a scratch Postgres (not the live `dragonsstash-db`), confirm the dump restores cleanly: + +```bash +ssh sam@192.168.68.68 "docker run -d --rm --name restore-drill-pg --network dragonsstash_internal \ + -e POSTGRES_USER=drill -e POSTGRES_PASSWORD=drill -e POSTGRES_DB=drill postgres:16-alpine" +ssh sam@192.168.68.68 "docker cp dragonsstash-backup:/tmp/restore-drill/tmp/dragonsstash.dump /tmp/dragonsstash.dump" +ssh sam@192.168.68.68 "docker cp /tmp/dragonsstash.dump restore-drill-pg:/tmp/dragonsstash.dump" +ssh sam@192.168.68.68 "docker exec -e PGPASSWORD=drill restore-drill-pg pg_restore -U drill -d drill --clean --if-exists /tmp/dragonsstash.dump" +ssh sam@192.168.68.68 "docker exec -e PGPASSWORD=drill restore-drill-pg psql -U drill -d drill -c '\\dt' | head -20" +ssh sam@192.168.68.68 "docker rm -f restore-drill-pg" +``` + +Expected: `pg_restore` completes without fatal errors; `\dt` lists the app's tables (e.g. `Package`, `User`, `TelegramLink`). + +- [ ] **Step 7: Clean up drill artifacts** + +```bash +ssh sam@192.168.68.68 "docker exec dragonsstash-backup rm -rf /tmp/restore-drill" +ssh sam@192.168.68.68 "rm -f /tmp/dragonsstash.dump" +```