fix: prevent system backups from swallowing backup_dir recursively (+ split config mounts) #128

Merged
fastie81 merged 2 commits from fix/backup-dir-collision into main 2026-09-05 20:46:33 +12:00
Owner

Root cause

On prod (Tower 192.168.10.10) the running container binds BOTH /app/backups and /app/app/data/content_images to the SAME host folder (/mnt/user/appdata/honbu-manager/backup). In create_system_backup() the content walk (os.walk(self.content_dir)) then embeds every prior system_backup_.zip inside the new archive - runaway GB growth. The job dies mid-write (OOM on the giant archive), leaving a truncated, unrestorable zip (no EOCD). All 3 existing system_backup_.zip on prod are corrupt (verified: 87M/126M/145M, all rejected by zipfile/unzip). The scheduled daily backup has silently failed since Jul 8.

Fixes (2 commits)

1. backup_service.py - code

  • Scratch dir: database.dump and club_data.json now write to tempfile.mkdtemp() OUTSIDE backup_dir.
  • Recursion guard: the content walk _is_within-filters anything under backup_dir and scratch, so an archive can never contain another archive regardless of how dirs are bound.
  • Atomic publish: zip written to .part then os.replace() to final name - a killed process can no longer leave a partial zip (.part cleaned in except handler).
  • Integrity check: zipfile.testzip() after close; fails loudly (and deletes) rather than silently accepting a corrupt archive.

2. docker-compose.yml + env templates - config

The compose fed both mounts from the SAME ${HOST_DATA_PATH} variable - the exact mis-config live on prod. Split into BACKUP_PATH (-> /app/backups) and CONTENT_PATH (-> /app/app/data/content_images) with distinct defaults so they can never collapse. Documented in .env.example, .env.prod, .env.test.

Test

  • Existing tests/unit/test_backup_service.py: 17 passed.
  • NEW regression test TestSystemBackupRecursionGuard reproduces the prod mis-config (content_dir == backup_dir), asserts the result is a valid archive, contains database.dump, contains NO embedded .zip, leaves no .part leftover and no stray database.dump. 18 passed total.

Follow-on (separate)

Deploy to prod requires setting BACKUP_PATH/CONTENT_PATH to distinct real paths in .env.prod and restarting the compose stack. Registry-based CI prod-deploy migration tracked separately.

## Root cause On prod (Tower 192.168.10.10) the running container binds BOTH /app/backups and /app/app/data/content_images to the SAME host folder (/mnt/user/appdata/honbu-manager/backup). In create_system_backup() the content walk (os.walk(self.content_dir)) then embeds every prior system_backup_*.zip inside the new archive - runaway GB growth. The job dies mid-write (OOM on the giant archive), leaving a truncated, unrestorable zip (no EOCD). All 3 existing system_backup_*.zip on prod are corrupt (verified: 87M/126M/145M, all rejected by zipfile/unzip). The scheduled daily backup has silently failed since Jul 8. ## Fixes (2 commits) ### 1. backup_service.py - code - Scratch dir: database.dump and club_data.json now write to tempfile.mkdtemp() OUTSIDE backup_dir. - Recursion guard: the content walk _is_within-filters anything under backup_dir and scratch, so an archive can never contain another archive regardless of how dirs are bound. - Atomic publish: zip written to <file>.part then os.replace() to final name - a killed process can no longer leave a partial zip (.part cleaned in except handler). - Integrity check: zipfile.testzip() after close; fails loudly (and deletes) rather than silently accepting a corrupt archive. ### 2. docker-compose.yml + env templates - config The compose fed both mounts from the SAME ${HOST_DATA_PATH} variable - the exact mis-config live on prod. Split into BACKUP_PATH (-> /app/backups) and CONTENT_PATH (-> /app/app/data/content_images) with distinct defaults so they can never collapse. Documented in .env.example, .env.prod, .env.test. ## Test - Existing tests/unit/test_backup_service.py: 17 passed. - NEW regression test TestSystemBackupRecursionGuard reproduces the prod mis-config (content_dir == backup_dir), asserts the result is a valid archive, contains database.dump, contains NO embedded .zip, leaves no .part leftover and no stray database.dump. 18 passed total. ## Follow-on (separate) Deploy to prod requires setting BACKUP_PATH/CONTENT_PATH to distinct real paths in .env.prod and restarting the compose stack. Registry-based CI prod-deploy migration tracked separately.
Root cause: when CONTENT_UPLOAD_DIR is bound to the same host folder as the
backup directory (as on prod), create_system_backup() walks and embeds prior
system_backup_*.zip files inside the new archive, causing runaway GB growth and
truncated/corrupt archives (killed mid-write, no EOCD).

Changes:
- Dump database and club JSON into a scratch dir outside backup_dir
- Skip backup_dir during the content walk (recursion guard via _is_within)
- Publish final zip with atomic .part -> rename (no partial zips)
- Verify integrity with zipfile.testzip(); fail loudly on corruption
- Regression test: content_dir == backup_dir still yields a valid archive
  that never contains another .zip
config: split backup_dir and content_dir mounts into separate vars
Some checks failed
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 55s
CI / lint-frontend (push) Successful in 2m37s
CI / lint-frontend (pull_request) Successful in 57s
CI / build-test (pull_request) Has been cancelled
CI / deploy-test (pull_request) Has been cancelled
CI / deploy-prod (pull_request) Has been cancelled
CI / build-release (pull_request) Has been cancelled
CI / build-test (push) Has been cancelled
CI / deploy-test (push) Has been cancelled
CI / deploy-prod (push) Has been cancelled
CI / build-release (push) Has been cancelled
b686a4ac26
The compose file fed both /app/backups and /app/app/data/content_images from
the same ${HOST_DATA_PATH} variable. If set, both collapses to one host folder;
that is the exact mis-config live on prod that made system backups recursively
embed themselves. Split into BACKUP_PATH and CONTENT_PATH with distinct
defaults, and document both in the env templates.
fastie81 force-pushed fix/backup-dir-collision from b686a4ac26
Some checks failed
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 55s
CI / lint-frontend (push) Successful in 2m37s
CI / lint-frontend (pull_request) Successful in 57s
CI / build-test (pull_request) Has been cancelled
CI / deploy-test (pull_request) Has been cancelled
CI / deploy-prod (pull_request) Has been cancelled
CI / build-release (pull_request) Has been cancelled
CI / build-test (push) Has been cancelled
CI / deploy-test (push) Has been cancelled
CI / deploy-prod (push) Has been cancelled
CI / build-release (push) Has been cancelled
to 1fe881643c
Some checks failed
CI / test (pull_request) Successful in 2m12s
CI / lint-frontend (pull_request) Successful in 1m1s
CI / build-release (push) Has been skipped
CI / build-test (pull_request) Failing after 1m25s
CI / build-release (pull_request) Has been skipped
CI / build-test (push) Successful in 2m49s
CI / deploy-prod (push) Has been skipped
CI / deploy-test (pull_request) Has been skipped
CI / deploy-prod (pull_request) Has been skipped
CI / deploy-test (push) Has been skipped
CI / test (push) Successful in 2m13s
CI / lint-frontend (push) Successful in 57s
2026-09-05 20:38:48 +12:00
Compare
Author
Owner

CI verified green on this PR (post-rebase onto main after #127 merged):

  • test ✅ (2m13s push / 2m12s PR)
  • lint-frontend ✅ (57s push / 1m1s PR)
  • build-test / build-release non-gating; deploy-test / deploy-prod correctly blocked (gated to protected branch/version tags)

Note: the push-run failure earlier was the pre-existing DATABASE_URL placeholder bug (#127), not this branch — resolved by merging #127 and rebasing. ~200 lines total (code + regression test + config split), mergeable clean against main.

CI verified green on this PR (post-rebase onto main after #127 merged): - `test` ✅ (2m13s push / 2m12s PR) - `lint-frontend` ✅ (57s push / 1m1s PR) - `build-test` / `build-release` non-gating; `deploy-test` / `deploy-prod` correctly blocked (gated to protected branch/version tags) Note: the push-run failure earlier was the pre-existing `DATABASE_URL` placeholder bug (#127), not this branch — resolved by merging #127 and rebasing. ~200 lines total (code + regression test + config split), mergeable clean against `main`.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
fastie81/honbu-manager!128
No description provided.