django-offsite-backup 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_offsite_backup-0.1.0/.gitignore +28 -0
- django_offsite_backup-0.1.0/AGENTS.md +76 -0
- django_offsite_backup-0.1.0/CHANGELOG.md +30 -0
- django_offsite_backup-0.1.0/CONTRIBUTING.md +52 -0
- django_offsite_backup-0.1.0/LICENSE +21 -0
- django_offsite_backup-0.1.0/PKG-INFO +133 -0
- django_offsite_backup-0.1.0/README.md +88 -0
- django_offsite_backup-0.1.0/SECURITY.md +22 -0
- django_offsite_backup-0.1.0/docs/wiki/AI-assistants.md +105 -0
- django_offsite_backup-0.1.0/docs/wiki/Changelog.md +58 -0
- django_offsite_backup-0.1.0/docs/wiki/Configuration-archive.md +106 -0
- django_offsite_backup-0.1.0/docs/wiki/Databases.md +143 -0
- django_offsite_backup-0.1.0/docs/wiki/Deploy-integration.md +121 -0
- django_offsite_backup-0.1.0/docs/wiki/Encryption.md +97 -0
- django_offsite_backup-0.1.0/docs/wiki/Installation.md +157 -0
- django_offsite_backup-0.1.0/docs/wiki/Media.md +140 -0
- django_offsite_backup-0.1.0/docs/wiki/Monitoring.md +97 -0
- django_offsite_backup-0.1.0/docs/wiki/Notifications.md +193 -0
- django_offsite_backup-0.1.0/docs/wiki/Restore.md +164 -0
- django_offsite_backup-0.1.0/docs/wiki/Retention.md +107 -0
- django_offsite_backup-0.1.0/docs/wiki/S3-providers.md +170 -0
- django_offsite_backup-0.1.0/docs/wiki/Scheduling.md +127 -0
- django_offsite_backup-0.1.0/docs/wiki/Security-model.md +108 -0
- django_offsite_backup-0.1.0/docs/wiki/Settings.md +274 -0
- django_offsite_backup-0.1.0/docs/wiki/Stores-S3.md +95 -0
- django_offsite_backup-0.1.0/docs/wiki/Stores-directory.md +60 -0
- django_offsite_backup-0.1.0/docs/wiki/System-checks.md +65 -0
- django_offsite_backup-0.1.0/docs/wiki/Testing.md +165 -0
- django_offsite_backup-0.1.0/docs/wiki/Troubleshooting.md +229 -0
- django_offsite_backup-0.1.0/docs/wiki/Upgrading.md +29 -0
- django_offsite_backup-0.1.0/docs/wiki/index.md +98 -0
- django_offsite_backup-0.1.0/mkdocs.yml +119 -0
- django_offsite_backup-0.1.0/pyproject.toml +176 -0
- django_offsite_backup-0.1.0/scripts/smoke_install.sh +51 -0
- django_offsite_backup-0.1.0/src/offsite_backup/__init__.py +52 -0
- django_offsite_backup-0.1.0/src/offsite_backup/apps.py +19 -0
- django_offsite_backup-0.1.0/src/offsite_backup/backend.py +48 -0
- django_offsite_backup-0.1.0/src/offsite_backup/check.py +65 -0
- django_offsite_backup-0.1.0/src/offsite_backup/checks.py +145 -0
- django_offsite_backup-0.1.0/src/offsite_backup/conf.py +212 -0
- django_offsite_backup-0.1.0/src/offsite_backup/config/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/config/base.py +20 -0
- django_offsite_backup-0.1.0/src/offsite_backup/config/paths.py +125 -0
- django_offsite_backup-0.1.0/src/offsite_backup/databases/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/databases/base.py +32 -0
- django_offsite_backup-0.1.0/src/offsite_backup/databases/postgres.py +204 -0
- django_offsite_backup-0.1.0/src/offsite_backup/encryption/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/encryption/age_cli.py +58 -0
- django_offsite_backup-0.1.0/src/offsite_backup/encryption/base.py +19 -0
- django_offsite_backup-0.1.0/src/offsite_backup/exceptions.py +49 -0
- django_offsite_backup-0.1.0/src/offsite_backup/guard.py +57 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/commands/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/commands/backup_check.py +27 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/commands/backup_list.py +86 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/commands/backup_offsite.py +49 -0
- django_offsite_backup-0.1.0/src/offsite_backup/management/commands/backup_restore.py +59 -0
- django_offsite_backup-0.1.0/src/offsite_backup/media/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/media/base.py +125 -0
- django_offsite_backup-0.1.0/src/offsite_backup/media/filesystem.py +75 -0
- django_offsite_backup-0.1.0/src/offsite_backup/media/s3.py +52 -0
- django_offsite_backup-0.1.0/src/offsite_backup/messages/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/messages/default.py +122 -0
- django_offsite_backup-0.1.0/src/offsite_backup/naming.py +119 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notifiers/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notifiers/base.py +16 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notifiers/django_aiogram.py +49 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notifiers/log.py +22 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notifiers/telegram.py +76 -0
- django_offsite_backup-0.1.0/src/offsite_backup/notify.py +96 -0
- django_offsite_backup-0.1.0/src/offsite_backup/outcome.py +56 -0
- django_offsite_backup-0.1.0/src/offsite_backup/pass_.py +265 -0
- django_offsite_backup-0.1.0/src/offsite_backup/pipeline.py +139 -0
- django_offsite_backup-0.1.0/src/offsite_backup/py.typed +0 -0
- django_offsite_backup-0.1.0/src/offsite_backup/registry.py +71 -0
- django_offsite_backup-0.1.0/src/offsite_backup/restore.py +198 -0
- django_offsite_backup-0.1.0/src/offsite_backup/rotation.py +95 -0
- django_offsite_backup-0.1.0/src/offsite_backup/schedule/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/schedule/celery.py +45 -0
- django_offsite_backup-0.1.0/src/offsite_backup/schedule/taskiq.py +35 -0
- django_offsite_backup-0.1.0/src/offsite_backup/sealing.py +25 -0
- django_offsite_backup-0.1.0/src/offsite_backup/stores/__init__.py +3 -0
- django_offsite_backup-0.1.0/src/offsite_backup/stores/base.py +67 -0
- django_offsite_backup-0.1.0/src/offsite_backup/stores/filesystem.py +90 -0
- django_offsite_backup-0.1.0/src/offsite_backup/stores/s3.py +260 -0
- django_offsite_backup-0.1.0/src/offsite_backup/testing/__init__.py +88 -0
- django_offsite_backup-0.1.0/src/offsite_backup/testing/fake_s3.py +274 -0
- django_offsite_backup-0.1.0/src/offsite_backup/testing/pytest_plugin.py +27 -0
- django_offsite_backup-0.1.0/tests/__init__.py +0 -0
- django_offsite_backup-0.1.0/tests/backends.py +69 -0
- django_offsite_backup-0.1.0/tests/conftest.py +77 -0
- django_offsite_backup-0.1.0/tests/integration/__init__.py +0 -0
- django_offsite_backup-0.1.0/tests/integration/test_round_trip.py +253 -0
- django_offsite_backup-0.1.0/tests/integration_settings.py +17 -0
- django_offsite_backup-0.1.0/tests/settings.py +5 -0
- django_offsite_backup-0.1.0/tests/test_checks_commands.py +388 -0
- django_offsite_backup-0.1.0/tests/test_conf.py +88 -0
- django_offsite_backup-0.1.0/tests/test_media_config.py +157 -0
- django_offsite_backup-0.1.0/tests/test_messages.py +102 -0
- django_offsite_backup-0.1.0/tests/test_naming.py +51 -0
- django_offsite_backup-0.1.0/tests/test_notify_guard_check.py +251 -0
- django_offsite_backup-0.1.0/tests/test_pass.py +241 -0
- django_offsite_backup-0.1.0/tests/test_pipeline.py +47 -0
- django_offsite_backup-0.1.0/tests/test_rotation.py +174 -0
- django_offsite_backup-0.1.0/tests/test_store_s3.py +136 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.idea/
|
|
2
|
+
.vscode/
|
|
3
|
+
venv/
|
|
4
|
+
.venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
|
|
7
|
+
# Python cache.
|
|
8
|
+
*.py[cod]
|
|
9
|
+
__pycache__/
|
|
10
|
+
|
|
11
|
+
# build
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
# the documentation site, built from docs/wiki/ by `mkdocs build`
|
|
15
|
+
site/
|
|
16
|
+
|
|
17
|
+
# tooling caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# JetBrains AI plugin checkpoints: 45 MB of local cache, not source
|
|
25
|
+
.proxyai/
|
|
26
|
+
|
|
27
|
+
# macOS
|
|
28
|
+
.DS_Store
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for coding agents working on **this repository**. For wiring the package into a
|
|
4
|
+
project, see the wiki page [AI assistants](https://corneizer.github.io/django-offsite-backup/latest/AI-assistants/).
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
A Django app that backs up databases, media and deployment configuration off the machine:
|
|
9
|
+
compress, then encrypt to public keys, upload to a store that locks every object, rotate by
|
|
10
|
+
whole nights, and say the outcome. Every backend is a dotted path in `OFFSITE_BACKUP` and is
|
|
11
|
+
imported only when named.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
src/offsite_backup/
|
|
15
|
+
__init__.py lazy exports (PEP 562), __version__
|
|
16
|
+
apps.py registers the checks, imports nothing else
|
|
17
|
+
conf.py the setting: defaults, on/off/partly-configured
|
|
18
|
+
registry.py SHIPPED (path -> driver, extra), build()
|
|
19
|
+
backend.py what every backend shares (REQUIRED, SIGNALS, BINARIES, checks)
|
|
20
|
+
checks.py system checks; network ones are deploy checks
|
|
21
|
+
naming.py object names; the night is read from the name
|
|
22
|
+
pipeline.py subprocess pipelines with pipefail semantics, no shell
|
|
23
|
+
sealing.py compress THEN encrypt
|
|
24
|
+
pass_.py one pass, back_up_database, keep_apart
|
|
25
|
+
rotation.py whole-night rotation and depth
|
|
26
|
+
guard.py once per UTC night
|
|
27
|
+
check.py the morning check
|
|
28
|
+
restore.py download, decrypt, verify, load
|
|
29
|
+
notify.py formatter + notifiers; silence() and capture()
|
|
30
|
+
outcome.py the records messages are written from
|
|
31
|
+
messages/ the shipped format
|
|
32
|
+
notifiers/ databases/ media/ config/ encryption/ stores/ one backend per module
|
|
33
|
+
schedule/ Celery and taskiq adapters, imported only by projects using them
|
|
34
|
+
management/commands/ backup_offsite, backup_check, backup_list, backup_restore
|
|
35
|
+
testing/ FakeS3, Passthrough, use_fake_s3, the runner and the pytest plugin
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
uv venv && uv pip install -e '.[all]' --group dev
|
|
42
|
+
ruff check . && ruff format --check . && mypy && python -m pytest -q
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Integration (real PostgreSQL, an S3 server with Object Lock, `age`, `zstd`) — see
|
|
46
|
+
`CONTRIBUTING.md`:
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
OFFSITE_BACKUP_TEST_S3_ENDPOINT=http://127.0.0.1:9000 OFFSITE_BACKUP_TEST_PG_PORT=5432 \
|
|
50
|
+
python -m pytest -m integration --ds=tests.integration_settings tests/integration
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
|
|
55
|
+
- **Nothing happens at import time.** `import offsite_backup` loads no driver; every backend
|
|
56
|
+
imports its driver inside the function that needs it. `tests/test_checks_commands.py`
|
|
57
|
+
imports every module with every driver blocked.
|
|
58
|
+
- **No private references.** This package is public: no company, client, product, host or
|
|
59
|
+
ticket names in code, docs, tests or commit messages. Examples use `acme/prod`.
|
|
60
|
+
- **Every change carries a test that fails without it.** Swap the source, watch it go red,
|
|
61
|
+
put it back — never `git checkout` a file with uncommitted work. One case per guard; no
|
|
62
|
+
case that fails for an earlier reason.
|
|
63
|
+
- **A notifier never raises and never costs the backup**; it logs the error class, never the
|
|
64
|
+
text it sends or a token.
|
|
65
|
+
- **Secrets travel in the environment, never in argv**: argv is visible in `ps`, and error
|
|
66
|
+
text is posted to a chat.
|
|
67
|
+
- **The lock is an argument of every upload and is read back.** Do not rely on a bucket
|
|
68
|
+
default for a single PUT.
|
|
69
|
+
- **Rotation is scoped to `<prefix>/<kind>/` with the trailing slash**, counts nights read
|
|
70
|
+
from names, deletes versions (never a bare key), and leaves locked versions for later.
|
|
71
|
+
- A settings rename or a default flip is a major version.
|
|
72
|
+
|
|
73
|
+
## Style
|
|
74
|
+
|
|
75
|
+
Code, comments, docstrings and docs in English. A comment is one line for the non-obvious
|
|
76
|
+
*why*; module docstrings explain design. Commits and pull requests carry no AI attribution.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The project follows
|
|
4
|
+
[semantic versioning](https://semver.org/): renaming a setting or flipping a default is a
|
|
5
|
+
major version.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
- A pass: PostgreSQL databases (a list, or computed at run time; plain or custom format),
|
|
12
|
+
media in volumes of whole files (a directory, a Docker volume mounted as one, or an S3
|
|
13
|
+
bucket), and the deployment configuration with the environment rebuilt from a list of
|
|
14
|
+
names — compressed with zstd, encrypted with age to one or more recipients.
|
|
15
|
+
- Stores: S3 with Object Lock COMPLIANCE passed on every upload and read back, versions and
|
|
16
|
+
delete markers, pagination; a local directory (no immutability, W001).
|
|
17
|
+
- Whole-night rotation per kind; passes without media (`--without-media`) never count as a
|
|
18
|
+
night; depth reported after rotation.
|
|
19
|
+
- One database failing does not cost the others their night; the pass fails at the end
|
|
20
|
+
naming each one.
|
|
21
|
+
- Once-per-night guard for the nightly pass; the morning check (MISSING / UNCHECKED).
|
|
22
|
+
- Messages in one HTML format; notifiers: log, Telegram (`[telegram]`), django-aiogram
|
|
23
|
+
(`[aiogram]`). Notifiers are silenced for every test run.
|
|
24
|
+
- `back_up_database` (extra recipients, `keep_apart`), `keep_apart`, `restore_database`.
|
|
25
|
+
- Commands: `backup_offsite`, `backup_check`, `backup_list`, `backup_restore` (download,
|
|
26
|
+
decrypt, verify; `--apply` loads a database).
|
|
27
|
+
- System checks E001–E008, W001–W005.
|
|
28
|
+
- Celery and taskiq adapters.
|
|
29
|
+
- `offsite_backup.testing`: FakeS3, Passthrough, `use_fake_s3`, `capture`, the test runner
|
|
30
|
+
and the pytest plugin.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Set up
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
uv venv
|
|
7
|
+
uv pip install -e '.[all]' --group dev # pip 25.1+ also understands --group
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## The gate
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
ruff check . && ruff format --check . && mypy && python -m pytest -q
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The unit suite needs no network and no binary beyond a POSIX shell: it runs against an
|
|
17
|
+
in-memory S3 (`offsite_backup.testing.FakeS3`) and a pass-through sealer.
|
|
18
|
+
|
|
19
|
+
## Integration
|
|
20
|
+
|
|
21
|
+
A full cycle against real services — backup, restore into a clean database, row counts of
|
|
22
|
+
every table compared; media volumes byte for byte; Object Lock read back; rotation over real
|
|
23
|
+
versions and delete markers.
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
docker run -d --rm --name ob-pg -e POSTGRES_USER=ob -e POSTGRES_PASSWORD=ob -e POSTGRES_DB=source \
|
|
27
|
+
-p 5432:5432 postgres:17
|
|
28
|
+
docker exec ob-pg psql -U ob -d source -c 'CREATE DATABASE target'
|
|
29
|
+
docker run -d --rm --name ob-s3 -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=minio12345 \
|
|
30
|
+
-p 9000:9000 cgr.dev/chainguard/minio:latest server /data
|
|
31
|
+
|
|
32
|
+
OFFSITE_BACKUP_TEST_S3_ENDPOINT=http://127.0.0.1:9000 \
|
|
33
|
+
python -m pytest -m integration --ds=tests.integration_settings tests/integration
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It needs `age`, `age-keygen`, `zstd`, `pg_dump`, `psql` and `pg_restore` on PATH, the client
|
|
37
|
+
at least as new as the server. Variables: `OFFSITE_BACKUP_TEST_PG_{HOST,PORT,USER,PASSWORD,SOURCE,TARGET}`,
|
|
38
|
+
`OFFSITE_BACKUP_TEST_S3_{ENDPOINT,ACCESS_KEY,SECRET_KEY}`. Each run creates fresh buckets:
|
|
39
|
+
COMPLIANCE locks cannot be lifted, so do not point it at a store you care about.
|
|
40
|
+
|
|
41
|
+
## Pull requests
|
|
42
|
+
|
|
43
|
+
One change per pull request, with a test that fails without it, and the wiki page that
|
|
44
|
+
describes the behaviour updated in the same pull request (`docs/wiki/`, built with
|
|
45
|
+
`mkdocs build --strict`). Add a line to `CHANGELOG.md`.
|
|
46
|
+
|
|
47
|
+
## Releasing
|
|
48
|
+
|
|
49
|
+
Bump `__version__` in `src/offsite_backup/__init__.py`, move the changelog section out of
|
|
50
|
+
"Unreleased", merge, then tag `vX.Y.Z` on master and push the tag. `publish.yml` checks the tag
|
|
51
|
+
against `__version__`, builds, smoke-installs the wheel, publishes to PyPI by trusted
|
|
52
|
+
publishing and creates the GitHub release.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleksii Kolosiuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-offsite-backup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Encrypted, immutable off-site backups of a Django project's databases, media and deployment configuration
|
|
5
|
+
Project-URL: Homepage, https://github.com/CorneiZeR/django-offsite-backup
|
|
6
|
+
Project-URL: Documentation, https://corneizer.github.io/django-offsite-backup/
|
|
7
|
+
Project-URL: Changelog, https://github.com/CorneiZeR/django-offsite-backup/blob/master/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/CorneiZeR/django-offsite-backup/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/CorneiZeR/django-offsite-backup
|
|
10
|
+
Author-email: Oleksii Kolosiuk <kolosyuk1@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: age,backup,django,encryption,object-lock,postgres,retention,s3,telegram
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Framework :: Django :: 6.0
|
|
18
|
+
Classifier: Framework :: Django :: 6.1
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Intended Audience :: System Administrators
|
|
21
|
+
Classifier: Operating System :: POSIX
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Classifier: Topic :: Database
|
|
30
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: <3.15,>=3.10
|
|
33
|
+
Requires-Dist: django>=5.2
|
|
34
|
+
Provides-Extra: aiogram
|
|
35
|
+
Requires-Dist: django-aiogram>=5.2; extra == 'aiogram'
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: boto3>=1.34; extra == 'all'
|
|
38
|
+
Requires-Dist: django-aiogram>=5.2; extra == 'all'
|
|
39
|
+
Requires-Dist: httpx2>=2.0; extra == 'all'
|
|
40
|
+
Provides-Extra: s3
|
|
41
|
+
Requires-Dist: boto3>=1.34; extra == 's3'
|
|
42
|
+
Provides-Extra: telegram
|
|
43
|
+
Requires-Dist: httpx2>=2.0; extra == 'telegram'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# django-offsite-backup
|
|
47
|
+
|
|
48
|
+
[](https://github.com/CorneiZeR/django-offsite-backup/actions/workflows/ci.yml)
|
|
49
|
+
[](https://pypi.org/project/django-offsite-backup/)
|
|
50
|
+
[](https://pypi.org/project/django-offsite-backup/)
|
|
51
|
+
|
|
52
|
+
Off-site backups of a Django project's **databases, media and deployment configuration**:
|
|
53
|
+
compressed, encrypted to a public key the server holds (so the server can write a backup
|
|
54
|
+
and cannot read one), uploaded to S3 with **Object Lock COMPLIANCE** read back on every
|
|
55
|
+
object, and rotated by **whole nights** — never by age alone, never by object count.
|
|
56
|
+
|
|
57
|
+
- PostgreSQL (one database, a list, or a list computed at run time), plain or custom format
|
|
58
|
+
- media from a directory, a Docker volume or an S3 bucket, in volumes of whole files
|
|
59
|
+
- the deployment directory plus the environment rebuilt from a list of variable names
|
|
60
|
+
- `age` encryption to one or more recipients; `zstd` compression before it
|
|
61
|
+
- a pass before every deploy without media (fast, and never mistaken for a night)
|
|
62
|
+
- a once-per-night guard, and a morning check that notices the night that never ran
|
|
63
|
+
- every outcome said in Telegram (or through django-aiogram, or a log), in one glanceable format
|
|
64
|
+
- restore that downloads, decrypts, verifies — and, on request, loads a database
|
|
65
|
+
- system checks that refuse a half-configured machine by name and a bucket without a lock
|
|
66
|
+
|
|
67
|
+
Every backend is a dotted path in settings and imported only when named; each driver is an
|
|
68
|
+
extra. A base install pulls Django and nothing else.
|
|
69
|
+
|
|
70
|
+
## 30-second setup
|
|
71
|
+
|
|
72
|
+
```shell
|
|
73
|
+
pip install "django-offsite-backup[s3,telegram]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
INSTALLED_APPS = [..., 'offsite_backup']
|
|
80
|
+
|
|
81
|
+
OFFSITE_BACKUP = {
|
|
82
|
+
'PREFIX': 'acme/prod',
|
|
83
|
+
'ENV': 'prod',
|
|
84
|
+
'STORE': {
|
|
85
|
+
'BACKEND': 'offsite_backup.stores.s3.S3Store',
|
|
86
|
+
'ENDPOINT_URL': os.environ.get('BACKUP_S3_ENDPOINT_URL', ''),
|
|
87
|
+
'BUCKET': os.environ.get('BACKUP_S3_BUCKET', ''),
|
|
88
|
+
'ACCESS_KEY': os.environ.get('BACKUP_S3_ACCESS_KEY', ''),
|
|
89
|
+
'SECRET_KEY': os.environ.get('BACKUP_S3_SECRET_KEY', ''),
|
|
90
|
+
'LOCK_DAYS': 13,
|
|
91
|
+
},
|
|
92
|
+
'ENCRYPTION': {
|
|
93
|
+
'BACKEND': 'offsite_backup.encryption.age_cli.AgeCli',
|
|
94
|
+
'RECIPIENTS': [os.environ.get('BACKUP_AGE_RECIPIENT', '')],
|
|
95
|
+
},
|
|
96
|
+
'DATABASES': {'BACKEND': 'offsite_backup.databases.postgres.PostgresSource'},
|
|
97
|
+
'MEDIA': [{'BACKEND': 'offsite_backup.media.filesystem.DirectorySource'}],
|
|
98
|
+
'CONFIG': {'BACKEND': 'offsite_backup.config.paths.PathsSource', 'PATHS': ['/app/deploy']},
|
|
99
|
+
'RETENTION': {'KEEP_NIGHTS': 14},
|
|
100
|
+
'NOTIFY': [
|
|
101
|
+
{
|
|
102
|
+
'BACKEND': 'offsite_backup.notifiers.telegram.TelegramNotifier',
|
|
103
|
+
'TOKEN': os.environ.get('BACKUP_TG_TOKEN', ''),
|
|
104
|
+
'CHAT_ID': os.environ.get('BACKUP_TG_CHAT_ID', ''),
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
With every credential empty (a development machine) backups are **off** and the commands
|
|
111
|
+
say "nothing to do". With some set and some missing, `manage.py check` names each one.
|
|
112
|
+
|
|
113
|
+
```shell
|
|
114
|
+
python manage.py check --deploy # bucket lock, pg_dump version
|
|
115
|
+
python manage.py backup_offsite # a pass now
|
|
116
|
+
python manage.py backup_offsite --without-media --reason deploy # before a migration
|
|
117
|
+
python manage.py backup_check # the morning check
|
|
118
|
+
python manage.py backup_list
|
|
119
|
+
python manage.py backup_restore --stamp latest --identity key.txt --into ./restore
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The image needs `pg_dump` (at least the server's major), `zstd` and `age`. The bucket must be
|
|
123
|
+
created **with Object Lock** and a default COMPLIANCE rule of `LOCK_DAYS` days.
|
|
124
|
+
|
|
125
|
+
## Documentation
|
|
126
|
+
|
|
127
|
+
**<https://corneizer.github.io/django-offsite-backup/>** — installation, every setting,
|
|
128
|
+
S3 providers, retention and locks, restore drills, the security model, troubleshooting by
|
|
129
|
+
check code, and a page for AI assistants wiring the package into a project.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# django-offsite-backup
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CorneiZeR/django-offsite-backup/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/django-offsite-backup/)
|
|
5
|
+
[](https://pypi.org/project/django-offsite-backup/)
|
|
6
|
+
|
|
7
|
+
Off-site backups of a Django project's **databases, media and deployment configuration**:
|
|
8
|
+
compressed, encrypted to a public key the server holds (so the server can write a backup
|
|
9
|
+
and cannot read one), uploaded to S3 with **Object Lock COMPLIANCE** read back on every
|
|
10
|
+
object, and rotated by **whole nights** — never by age alone, never by object count.
|
|
11
|
+
|
|
12
|
+
- PostgreSQL (one database, a list, or a list computed at run time), plain or custom format
|
|
13
|
+
- media from a directory, a Docker volume or an S3 bucket, in volumes of whole files
|
|
14
|
+
- the deployment directory plus the environment rebuilt from a list of variable names
|
|
15
|
+
- `age` encryption to one or more recipients; `zstd` compression before it
|
|
16
|
+
- a pass before every deploy without media (fast, and never mistaken for a night)
|
|
17
|
+
- a once-per-night guard, and a morning check that notices the night that never ran
|
|
18
|
+
- every outcome said in Telegram (or through django-aiogram, or a log), in one glanceable format
|
|
19
|
+
- restore that downloads, decrypts, verifies — and, on request, loads a database
|
|
20
|
+
- system checks that refuse a half-configured machine by name and a bucket without a lock
|
|
21
|
+
|
|
22
|
+
Every backend is a dotted path in settings and imported only when named; each driver is an
|
|
23
|
+
extra. A base install pulls Django and nothing else.
|
|
24
|
+
|
|
25
|
+
## 30-second setup
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
pip install "django-offsite-backup[s3,telegram]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
INSTALLED_APPS = [..., 'offsite_backup']
|
|
35
|
+
|
|
36
|
+
OFFSITE_BACKUP = {
|
|
37
|
+
'PREFIX': 'acme/prod',
|
|
38
|
+
'ENV': 'prod',
|
|
39
|
+
'STORE': {
|
|
40
|
+
'BACKEND': 'offsite_backup.stores.s3.S3Store',
|
|
41
|
+
'ENDPOINT_URL': os.environ.get('BACKUP_S3_ENDPOINT_URL', ''),
|
|
42
|
+
'BUCKET': os.environ.get('BACKUP_S3_BUCKET', ''),
|
|
43
|
+
'ACCESS_KEY': os.environ.get('BACKUP_S3_ACCESS_KEY', ''),
|
|
44
|
+
'SECRET_KEY': os.environ.get('BACKUP_S3_SECRET_KEY', ''),
|
|
45
|
+
'LOCK_DAYS': 13,
|
|
46
|
+
},
|
|
47
|
+
'ENCRYPTION': {
|
|
48
|
+
'BACKEND': 'offsite_backup.encryption.age_cli.AgeCli',
|
|
49
|
+
'RECIPIENTS': [os.environ.get('BACKUP_AGE_RECIPIENT', '')],
|
|
50
|
+
},
|
|
51
|
+
'DATABASES': {'BACKEND': 'offsite_backup.databases.postgres.PostgresSource'},
|
|
52
|
+
'MEDIA': [{'BACKEND': 'offsite_backup.media.filesystem.DirectorySource'}],
|
|
53
|
+
'CONFIG': {'BACKEND': 'offsite_backup.config.paths.PathsSource', 'PATHS': ['/app/deploy']},
|
|
54
|
+
'RETENTION': {'KEEP_NIGHTS': 14},
|
|
55
|
+
'NOTIFY': [
|
|
56
|
+
{
|
|
57
|
+
'BACKEND': 'offsite_backup.notifiers.telegram.TelegramNotifier',
|
|
58
|
+
'TOKEN': os.environ.get('BACKUP_TG_TOKEN', ''),
|
|
59
|
+
'CHAT_ID': os.environ.get('BACKUP_TG_CHAT_ID', ''),
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With every credential empty (a development machine) backups are **off** and the commands
|
|
66
|
+
say "nothing to do". With some set and some missing, `manage.py check` names each one.
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
python manage.py check --deploy # bucket lock, pg_dump version
|
|
70
|
+
python manage.py backup_offsite # a pass now
|
|
71
|
+
python manage.py backup_offsite --without-media --reason deploy # before a migration
|
|
72
|
+
python manage.py backup_check # the morning check
|
|
73
|
+
python manage.py backup_list
|
|
74
|
+
python manage.py backup_restore --stamp latest --identity key.txt --into ./restore
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The image needs `pg_dump` (at least the server's major), `zstd` and `age`. The bucket must be
|
|
78
|
+
created **with Object Lock** and a default COMPLIANCE rule of `LOCK_DAYS` days.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
**<https://corneizer.github.io/django-offsite-backup/>** — installation, every setting,
|
|
83
|
+
S3 providers, retention and locks, restore drills, the security model, troubleshooting by
|
|
84
|
+
check code, and a page for AI assistants wiring the package into a project.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security issues privately through GitHub's
|
|
6
|
+
[private vulnerability reporting](https://github.com/CorneiZeR/django-offsite-backup/security/advisories/new),
|
|
7
|
+
not in a public issue. Expect an acknowledgement within a few days.
|
|
8
|
+
|
|
9
|
+
## Supported versions
|
|
10
|
+
|
|
11
|
+
The latest minor release receives security fixes.
|
|
12
|
+
|
|
13
|
+
## What the package promises
|
|
14
|
+
|
|
15
|
+
- The server holds **public** keys only; a backup it wrote cannot be read with anything on it.
|
|
16
|
+
- Every object in an S3 store is written with Object Lock COMPLIANCE and the lock is read back;
|
|
17
|
+
the key the server holds cannot delete a locked object.
|
|
18
|
+
- Credentials reach subprocesses through the environment, never the command line, and no
|
|
19
|
+
notifier logs a token or the message body.
|
|
20
|
+
|
|
21
|
+
The [Security model](https://corneizer.github.io/django-offsite-backup/latest/Security-model/)
|
|
22
|
+
page describes the threat model and its limits.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# AI assistants
|
|
2
|
+
|
|
3
|
+
Coding agents wire this package into a project the way a person would — by reading the
|
|
4
|
+
docs — and the mistakes they make are the expensive kind: a private key on the server, a
|
|
5
|
+
test suite posting to the team's chat, a bucket without Object Lock. This page is the
|
|
6
|
+
brief to hand them.
|
|
7
|
+
|
|
8
|
+
## Paste this in
|
|
9
|
+
|
|
10
|
+
Short enough for a system prompt or a `CLAUDE.md` / `AGENTS.md` / `.cursor/rules` file:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
Project uses django-offsite-backup 0.1. Rules:
|
|
14
|
+
|
|
15
|
+
- Install with the extras the configuration names, nothing inferred:
|
|
16
|
+
`django-offsite-backup[s3,telegram]` for an S3 store and Telegram messages,
|
|
17
|
+
`[s3,aiogram]` to send through the project's django-aiogram bot. Add
|
|
18
|
+
'offsite_backup' to INSTALLED_APPS. It has no models and no migrations.
|
|
19
|
+
- The image that runs a pass needs the binaries: pg_dump (major >= the
|
|
20
|
+
PostgreSQL server's), zstd, age. Not tar. Install them in the worker's image
|
|
21
|
+
AND the image the deploy pass runs in.
|
|
22
|
+
- Settings are one dict, OFFSITE_BACKUP, in settings.py. Read EVERY credential
|
|
23
|
+
from the environment with a default of '' — os.environ.get('X', '') — never
|
|
24
|
+
os.environ['X'] and never a placeholder value. Empty means "backups off on this
|
|
25
|
+
machine", which is what developer laptops and CI want.
|
|
26
|
+
- The age RECIPIENTS is a public key and may be read from the environment or
|
|
27
|
+
written into settings; it does not switch backups on by itself.
|
|
28
|
+
- Backend paths, exactly:
|
|
29
|
+
offsite_backup.stores.s3.S3Store / offsite_backup.stores.filesystem.DirectoryStore
|
|
30
|
+
offsite_backup.encryption.age_cli.AgeCli
|
|
31
|
+
offsite_backup.databases.postgres.PostgresSource
|
|
32
|
+
offsite_backup.media.filesystem.DirectorySource / offsite_backup.media.s3.S3Source
|
|
33
|
+
offsite_backup.config.paths.PathsSource
|
|
34
|
+
offsite_backup.notifiers.telegram.TelegramNotifier
|
|
35
|
+
offsite_backup.notifiers.django_aiogram.TopicNotifier
|
|
36
|
+
offsite_backup.notifiers.log.LogNotifier
|
|
37
|
+
- NEVER put an age private key (AGE-SECRET-KEY-...) on the server, in settings,
|
|
38
|
+
in .env, in CI variables, or in the repository. RECIPIENTS takes public keys
|
|
39
|
+
(age1...). The private key is generated on a person's machine with age-keygen
|
|
40
|
+
and kept in their password manager. E008 refuses a private key in RECIPIENTS.
|
|
41
|
+
Do not generate a key pair for the user: tell them to run age-keygen and give
|
|
42
|
+
you the public half.
|
|
43
|
+
- The S3 bucket must be created WITH Object Lock (it cannot be enabled later)
|
|
44
|
+
and given a default retention rule COMPLIANCE, days == STORE['LOCK_DAYS']
|
|
45
|
+
(13). Keep RETENTION['KEEP_NIGHTS'] (14) greater than LOCK_DAYS. Never
|
|
46
|
+
suggest GOVERNANCE mode. Never "fix" E005/E006 by setting LOCK_DAYS to 0
|
|
47
|
+
without the user explicitly accepting backups that the server can delete.
|
|
48
|
+
- PREFIX is required ('<project>/<env>', e.g. 'acme/prod'). Rotation never
|
|
49
|
+
leaves it.
|
|
50
|
+
- Scheduling: Celery -> CELERY_BEAT_SCHEDULE = {**beat_schedule(), ...} from
|
|
51
|
+
offsite_backup.schedule.celery, with CELERY_IMPORTS including
|
|
52
|
+
'offsite_backup.schedule.celery' and the beat in UTC. taskiq ->
|
|
53
|
+
register(broker) from offsite_backup.schedule.taskiq. cron -> 03:00 UTC
|
|
54
|
+
`manage.py backup_offsite --reason nightly`, 09:00 UTC `manage.py backup_check`.
|
|
55
|
+
The guard needs GUARD['CACHE'] to be a cache shared by every worker (Redis,
|
|
56
|
+
Memcached, database) — not LocMemCache (W003).
|
|
57
|
+
- Deploy: run `python manage.py backup_offsite --without-media --reason deploy`
|
|
58
|
+
on the NEW image BEFORE `migrate`, and let a non-zero exit stop the deploy.
|
|
59
|
+
Do not add `|| true`. Mount the deployment directory read-only into the service
|
|
60
|
+
that runs it AND into the worker.
|
|
61
|
+
- Validate with `python manage.py check` and `python manage.py check --deploy`.
|
|
62
|
+
Treat offsite_backup.E0xx/W0xx as the spec. Do not silence an error; only
|
|
63
|
+
W001 (directory store) may be silenced, and only where the user chose it.
|
|
64
|
+
- Tests must never notify. pytest is covered by the package's plugin
|
|
65
|
+
automatically; with Django's runner set
|
|
66
|
+
TEST_RUNNER = 'offsite_backup.testing.OffsiteBackupTestRunner' (or mix it in).
|
|
67
|
+
Do not give CI test jobs the production notifier token or store keys. In
|
|
68
|
+
tests use offsite_backup.testing: use_fake_s3(FakeS3()), Passthrough as the
|
|
69
|
+
ENCRYPTION backend with COMPRESS [], capture() to assert on messages,
|
|
70
|
+
media_tree() for media. Passthrough is for tests only (W002).
|
|
71
|
+
- Restore: `manage.py backup_restore --stamp latest --into DIR` with the
|
|
72
|
+
private key in AGE_IDENTITY or --identity FILE, run by a person on their own
|
|
73
|
+
machine. Never write code that stores the identity on the server.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Prompts that work
|
|
77
|
+
|
|
78
|
+
**Wire it in.** *"Add django-offsite-backup to this project: S3 store with credentials
|
|
79
|
+
from `BACKUP_S3_*` variables, age recipient from `BACKUP_AGE_RECIPIENT`, PostgreSQL
|
|
80
|
+
`default`, media from `MEDIA_ROOT`, the `deploy/` directory as configuration, Telegram from
|
|
81
|
+
`BACKUP_TELEGRAM_*`. Add the Celery beat entries, the pre-migration step to our deploy script,
|
|
82
|
+
the read-only mount on web and worker, the binaries to the Dockerfile, and
|
|
83
|
+
`OffsiteBackupTestRunner`. Do not generate keys. Show me `manage.py check` output with the
|
|
84
|
+
variables unset."* — the last sentence is the test that the laptop stays "off".
|
|
85
|
+
|
|
86
|
+
**Review a wiring.** *"Check our OFFSITE_BACKUP settings and deploy script against the
|
|
87
|
+
django-offsite-backup docs: credentials from the environment with '' defaults, no private
|
|
88
|
+
key anywhere, backup before migrate with a failing exit stopping the deploy, mounts on
|
|
89
|
+
both services, notifier silenced in tests."*
|
|
90
|
+
|
|
91
|
+
**Prepare a bucket.** *"Give me the AWS CLI commands to create a bucket with Object Lock and
|
|
92
|
+
a default COMPLIANCE rule of 13 days on <provider>, and a least-privilege policy for the
|
|
93
|
+
backup key under prefix `acme/prod/`. Do not run them."*
|
|
94
|
+
|
|
95
|
+
## What to check after an agent is done
|
|
96
|
+
|
|
97
|
+
* `grep -rn "AGE-SECRET-KEY" .` finds nothing — in the repository, the CI configuration or
|
|
98
|
+
the server's `.env`.
|
|
99
|
+
* `python manage.py check` with the backup variables **unset** reports no `offsite_backup`
|
|
100
|
+
code (the machine is off); with them set, none but warnings you chose.
|
|
101
|
+
* `python manage.py check --deploy` on the server: no `E004`, `E005`, `E006`.
|
|
102
|
+
* The deploy script stops when `backup_offsite` exits non-zero — try it once with a wrong
|
|
103
|
+
bucket name on staging.
|
|
104
|
+
* The test job's environment holds no production notifier token.
|
|
105
|
+
* Someone has restored a backup — [Restore](Restore.md).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — unreleased
|
|
4
|
+
|
|
5
|
+
The first release.
|
|
6
|
+
|
|
7
|
+
**A pass.** `manage.py backup_offsite` (and `offsite_backup.run()`) backs up the
|
|
8
|
+
databases, the media and the deployment configuration, one part at a time, then rotates.
|
|
9
|
+
A database that fails does not cost the others their night; the pass fails at the end
|
|
10
|
+
naming every one. `--without-media` and `--only` for partial passes; `--reason` for the
|
|
11
|
+
message.
|
|
12
|
+
|
|
13
|
+
**Databases.** `offsite_backup.databases.postgres.PostgresSource`: one alias, a list, or a
|
|
14
|
+
callable that computes them at run time; plain SQL (`.sql.zst.age`) or the custom format
|
|
15
|
+
(`.dump.zst.age`, `pg_dump`'s compression off, then zstd). The password travels in
|
|
16
|
+
`PGPASSWORD`, never on a command line. `back_up_database()` for a one-off dump with extra
|
|
17
|
+
recipients, and `keep_apart` to set one aside from rotation.
|
|
18
|
+
|
|
19
|
+
**Media.** `offsite_backup.media.filesystem.DirectorySource` and
|
|
20
|
+
`offsite_backup.media.s3.S3Source`: volumes of whole files (4 GiB by default), each a
|
|
21
|
+
complete tar written with Python's `tarfile`, streamed one at a time; `INCLUDE`/`EXCLUDE`
|
|
22
|
+
globs; several sources.
|
|
23
|
+
|
|
24
|
+
**Configuration.** `offsite_backup.config.paths.PathsSource`: listed paths plus the
|
|
25
|
+
environment rebuilt from a list of names, written last.
|
|
26
|
+
|
|
27
|
+
**Encryption.** `offsite_backup.encryption.age_cli.AgeCli`: compress, then encrypt, to one
|
|
28
|
+
or more public keys, under pipefail semantics.
|
|
29
|
+
|
|
30
|
+
**Stores.** `offsite_backup.stores.s3.S3Store`: every upload locked COMPLIANCE by its own
|
|
31
|
+
arguments and read back; multipart in 64 MiB parts; versions and delete markers handled;
|
|
32
|
+
pagination everywhere; only the configured credentials.
|
|
33
|
+
`offsite_backup.stores.filesystem.DirectoryStore`: atomic writes, `0600` files, and a
|
|
34
|
+
warning that it has no immutability.
|
|
35
|
+
|
|
36
|
+
**Retention.** Whole UTC nights read from object names, per kind; passes without media never
|
|
37
|
+
count as nights; every version deleted; locked versions left for a later night; depth in
|
|
38
|
+
the message.
|
|
39
|
+
|
|
40
|
+
**Messages.** One HTML format for written, failed, missing and unchecked, every value
|
|
41
|
+
escaped. Notifiers: `LogNotifier`, `TelegramNotifier` (`[telegram]`, httpx2),
|
|
42
|
+
`TopicNotifier` through django-aiogram (`[aiogram]`). A notifier never costs the backup.
|
|
43
|
+
|
|
44
|
+
**Scheduling.** `run_nightly()` with a once-per-night guard in a shared cache; Celery
|
|
45
|
+
(`offsite_backup.schedule.celery.beat_schedule`) and taskiq
|
|
46
|
+
(`offsite_backup.schedule.taskiq.register`) adapters.
|
|
47
|
+
|
|
48
|
+
**Monitoring.** `manage.py backup_check` — the morning check over the configuration folder
|
|
49
|
+
and every database's; `LAST_SUCCESS_FILE`; `manage.py backup_list`.
|
|
50
|
+
|
|
51
|
+
**Restore.** `manage.py backup_restore`: download, decrypt and verify every object of a
|
|
52
|
+
stamp; `--apply` loads a database (`--from-database`, `--clean`); `restore_database()`.
|
|
53
|
+
|
|
54
|
+
**System checks.** `E001`–`E008`, `W001`–`W005`; network checks under `check --deploy`
|
|
55
|
+
only.
|
|
56
|
+
|
|
57
|
+
**Testing.** A pytest plugin and `OffsiteBackupTestRunner` that silence network
|
|
58
|
+
notifiers; `capture()`, `FakeS3`, `use_fake_s3`, `Passthrough`, `media_tree`.
|