django-dbs 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_dbs-0.1.0/LICENSE +21 -0
- django_dbs-0.1.0/PKG-INFO +194 -0
- django_dbs-0.1.0/README.md +166 -0
- django_dbs-0.1.0/dbs/__init__.py +34 -0
- django_dbs-0.1.0/dbs/_cli.py +31 -0
- django_dbs-0.1.0/dbs/apps.py +17 -0
- django_dbs-0.1.0/dbs/container/__init__.py +29 -0
- django_dbs-0.1.0/dbs/container/blocks.py +202 -0
- django_dbs-0.1.0/dbs/container/format.py +155 -0
- django_dbs-0.1.0/dbs/contrib/__init__.py +10 -0
- django_dbs-0.1.0/dbs/contrib/admin.py +109 -0
- django_dbs-0.1.0/dbs/contrib/urls.py +12 -0
- django_dbs-0.1.0/dbs/crypto/__init__.py +25 -0
- django_dbs-0.1.0/dbs/crypto/envelope.py +131 -0
- django_dbs-0.1.0/dbs/crypto/kdf.py +62 -0
- django_dbs-0.1.0/dbs/engine/__init__.py +12 -0
- django_dbs-0.1.0/dbs/engine/backup.py +129 -0
- django_dbs-0.1.0/dbs/engine/payload.py +57 -0
- django_dbs-0.1.0/dbs/engine/restore.py +91 -0
- django_dbs-0.1.0/dbs/engine/validate.py +66 -0
- django_dbs-0.1.0/dbs/exceptions.py +42 -0
- django_dbs-0.1.0/dbs/files.py +150 -0
- django_dbs-0.1.0/dbs/introspect.py +135 -0
- django_dbs-0.1.0/dbs/management/__init__.py +0 -0
- django_dbs-0.1.0/dbs/management/commands/__init__.py +0 -0
- django_dbs-0.1.0/dbs/management/commands/dbs_backup.py +60 -0
- django_dbs-0.1.0/dbs/management/commands/dbs_restore.py +49 -0
- django_dbs-0.1.0/dbs/management/commands/dbs_validate.py +39 -0
- django_dbs-0.1.0/dbs/registry.py +94 -0
- django_dbs-0.1.0/dbs/serialize.py +52 -0
- django_dbs-0.1.0/dbs/transports/__init__.py +5 -0
- django_dbs-0.1.0/dbs/transports/ssh.py +145 -0
- django_dbs-0.1.0/django_dbs.egg-info/PKG-INFO +194 -0
- django_dbs-0.1.0/django_dbs.egg-info/SOURCES.txt +43 -0
- django_dbs-0.1.0/django_dbs.egg-info/dependency_links.txt +1 -0
- django_dbs-0.1.0/django_dbs.egg-info/requires.txt +12 -0
- django_dbs-0.1.0/django_dbs.egg-info/top_level.txt +1 -0
- django_dbs-0.1.0/pyproject.toml +41 -0
- django_dbs-0.1.0/setup.cfg +4 -0
- django_dbs-0.1.0/tests/test_admin.py +59 -0
- django_dbs-0.1.0/tests/test_commands.py +43 -0
- django_dbs-0.1.0/tests/test_container.py +92 -0
- django_dbs-0.1.0/tests/test_crypto.py +48 -0
- django_dbs-0.1.0/tests/test_engine.py +131 -0
- django_dbs-0.1.0/tests/test_ssh.py +38 -0
django_dbs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bn7ya
|
|
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,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-dbs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Redundant, self-healing, encrypted single-file backup for Django projects.
|
|
5
|
+
Author: bn7ya
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bn7ya/dbs
|
|
8
|
+
Keywords: django,backup,restore,encryption,reed-solomon,redundancy
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
13
|
+
Classifier: Topic :: Security :: Cryptography
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: Django>=4.2
|
|
18
|
+
Requires-Dist: cryptography>=41
|
|
19
|
+
Requires-Dist: argon2-cffi>=21.3
|
|
20
|
+
Requires-Dist: reedsolo>=1.7
|
|
21
|
+
Provides-Extra: ssh
|
|
22
|
+
Requires-Dist: paramiko>=3; extra == "ssh"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-django>=4; extra == "dev"
|
|
26
|
+
Requires-Dist: paramiko>=3; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# DBS — Django Backup Solution
|
|
30
|
+
|
|
31
|
+
A backup library you drop into a Django project's source. It reads your models,
|
|
32
|
+
relations and files and writes **one encrypted file** that is *redundant* and
|
|
33
|
+
*self-healing*: every backup stores **two copies** of the data plus
|
|
34
|
+
**Reed-Solomon parity**, so silent corruption — the kind a non-ECC RAM stick
|
|
35
|
+
produces — is detected and repaired on restore instead of quietly poisoning
|
|
36
|
+
your data.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
io ──► security (Argon2id + AES-256-GCM) ──► integrity (2 copies · per-block
|
|
40
|
+
hashes · Reed-Solomon) ──► data (models · relations · files)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Why not django-dbbackup?
|
|
44
|
+
|
|
45
|
+
`django-dbbackup` is great for "dump the DB + media and ship it somewhere".
|
|
46
|
+
DBS targets a different need:
|
|
47
|
+
|
|
48
|
+
| | django-dbbackup | **DBS** |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Two self-healing copies in one file | ✗ | ✅ |
|
|
51
|
+
| Reed-Solomon FEC for bit-rot | ✗ | ✅ |
|
|
52
|
+
| Per-field mapping (value / file / file-path) | ✗ | ✅ |
|
|
53
|
+
| Passphrase-derived key, **never stored** (Argon2id) | ✗ (GPG keys) | ✅ |
|
|
54
|
+
| Per-file + per-block integrity hashes | partial | ✅ |
|
|
55
|
+
| CLI · admin-UI download · SFTP | CLI · remote storages | ✅ |
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install django-dbs # core
|
|
61
|
+
pip install "django-dbs[ssh]" # + SFTP transport (paramiko)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Add the app:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
INSTALLED_APPS = [..., "dbs"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
### 1. (Optional) declare what to back up
|
|
73
|
+
|
|
74
|
+
By default DBS auto-discovers every model, treats `FileField`/`ImageField` as
|
|
75
|
+
files, and preserves relations. Register a model only to override the defaults —
|
|
76
|
+
in a `dbs.py` module inside your app (auto-discovered like `admin.py`):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# myapp/dbs.py
|
|
80
|
+
from dbs import backup_registry, FieldType, ModelBackup
|
|
81
|
+
from .models import Invoice
|
|
82
|
+
|
|
83
|
+
@backup_registry.register(Invoice)
|
|
84
|
+
class InvoiceBackup(ModelBackup):
|
|
85
|
+
overrides = {
|
|
86
|
+
"scanned_pdf_path": FieldType.FILE_PATH, # CharField holding a path -> embed the file
|
|
87
|
+
"render_cache": FieldType.EXCLUDE, # don't back this column up
|
|
88
|
+
}
|
|
89
|
+
file_roots = ["/srv/myapp/uploads"] # extra non-model file trees
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`FieldType` values: `VALUE` (default), `FILE` (embed a FileField's bytes),
|
|
93
|
+
`FILE_PATH` (a string column whose path's file is embedded), `EXCLUDE`.
|
|
94
|
+
|
|
95
|
+
### 2. Back up / restore / validate from the CLI
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python manage.py dbs_backup backup.dbs # prompts for a passphrase
|
|
99
|
+
python manage.py dbs_validate backup.dbs # structural check, no passphrase
|
|
100
|
+
python manage.py dbs_validate backup.dbs -p secret # + verify decryption
|
|
101
|
+
python manage.py dbs_restore backup.dbs # restore rows + files
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The passphrase comes from `--passphrase`, then `$DBS_PASSPHRASE`, then a prompt.
|
|
105
|
+
|
|
106
|
+
### 3. Download / upload from the admin UI
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# urls.py
|
|
110
|
+
urlpatterns += [path("dbs/", include("dbs.contrib.urls"))]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Superusers can then visit `/dbs/backup/` to download an encrypted backup and
|
|
114
|
+
`/dbs/restore/` to upload one. The passphrase is entered in the form and never
|
|
115
|
+
stored server-side.
|
|
116
|
+
|
|
117
|
+
### 4. Ship a backup to another server (SFTP)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# settings.py — profiles reference a key by path; no secrets embedded
|
|
121
|
+
DBS_SSH_TARGETS = {
|
|
122
|
+
"offsite": {
|
|
123
|
+
"host": "backups.example.com", "username": "deploy",
|
|
124
|
+
"key_filename": "/home/deploy/.ssh/id_ed25519",
|
|
125
|
+
"remote_dir": "/var/backups/myproject",
|
|
126
|
+
"known_hosts": "/home/deploy/.ssh/known_hosts",
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from dbs import create_backup
|
|
133
|
+
from dbs.transports import SSHTarget, push_backup
|
|
134
|
+
|
|
135
|
+
data = create_backup("my passphrase")
|
|
136
|
+
push_backup(data, "backup.dbs", SSHTarget.from_settings("offsite"))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Python API
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from dbs import create_backup, restore_backup, validate_backup
|
|
143
|
+
|
|
144
|
+
blob = create_backup("passphrase", output="backup.dbs")
|
|
145
|
+
report = validate_backup(blob, "passphrase") # report.ok / report.summary()
|
|
146
|
+
result = restore_backup(blob, "passphrase") # result.healed is True if it repaired corruption
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## How it heals
|
|
150
|
+
|
|
151
|
+
On write, the encrypted stream is split into blocks; each block gets a BLAKE2b
|
|
152
|
+
hash and a layer of Reed-Solomon parity, and the whole stream is stored **twice**
|
|
153
|
+
(plus the header and manifest are stored twice). On read, each block is taken
|
|
154
|
+
from whichever copy verifies; sparse bit-flips are corrected in place by
|
|
155
|
+
Reed-Solomon even when *both* copies are hit. Every recovered block is checked
|
|
156
|
+
against its stored hash, so a mis-correction can never slip through — and a
|
|
157
|
+
freshly written backup is re-read and verified end-to-end (**verify-after-write**)
|
|
158
|
+
before the command reports success.
|
|
159
|
+
|
|
160
|
+
What it can recover from: whole-block loss in one copy, and sparse byte errors
|
|
161
|
+
(up to the parity budget, ~8 bytes per 255-byte codeword by default) in both
|
|
162
|
+
copies. What it cannot: a block destroyed *beyond* the parity budget in **both**
|
|
163
|
+
copies — DBS then refuses to restore and reports exactly which blocks failed,
|
|
164
|
+
rather than producing silently wrong data.
|
|
165
|
+
|
|
166
|
+
## Security model
|
|
167
|
+
|
|
168
|
+
* **Argon2id** derives a key from your passphrase (memory-hard ⇒ brute-force
|
|
169
|
+
resistant). Raise `KDFParams` cost for more resistance.
|
|
170
|
+
* **Envelope encryption**: a random data key encrypts the payload with
|
|
171
|
+
**AES-256-GCM**; that data key is wrapped by the passphrase-derived key. The
|
|
172
|
+
file stores only the salt, Argon2 parameters and the wrapped key — **never the
|
|
173
|
+
passphrase and never the raw data key**.
|
|
174
|
+
* A wrong passphrase fails the GCM tag check and is reported as such; it can
|
|
175
|
+
never yield partial/garbage data.
|
|
176
|
+
|
|
177
|
+
## Settings reference
|
|
178
|
+
|
|
179
|
+
| Setting | Purpose |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `DBS_EXCLUDE_MODELS` | `["app.Model", ...]` to skip (defaults skip contenttypes, permissions, admin log, sessions). |
|
|
182
|
+
| `DBS_FILE_ROOTS` | Extra directories embedded in every backup. |
|
|
183
|
+
| `DBS_SSH_TARGETS` | Named SFTP connection profiles. |
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[dev]"
|
|
189
|
+
pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# DBS — Django Backup Solution
|
|
2
|
+
|
|
3
|
+
A backup library you drop into a Django project's source. It reads your models,
|
|
4
|
+
relations and files and writes **one encrypted file** that is *redundant* and
|
|
5
|
+
*self-healing*: every backup stores **two copies** of the data plus
|
|
6
|
+
**Reed-Solomon parity**, so silent corruption — the kind a non-ECC RAM stick
|
|
7
|
+
produces — is detected and repaired on restore instead of quietly poisoning
|
|
8
|
+
your data.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
io ──► security (Argon2id + AES-256-GCM) ──► integrity (2 copies · per-block
|
|
12
|
+
hashes · Reed-Solomon) ──► data (models · relations · files)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why not django-dbbackup?
|
|
16
|
+
|
|
17
|
+
`django-dbbackup` is great for "dump the DB + media and ship it somewhere".
|
|
18
|
+
DBS targets a different need:
|
|
19
|
+
|
|
20
|
+
| | django-dbbackup | **DBS** |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Two self-healing copies in one file | ✗ | ✅ |
|
|
23
|
+
| Reed-Solomon FEC for bit-rot | ✗ | ✅ |
|
|
24
|
+
| Per-field mapping (value / file / file-path) | ✗ | ✅ |
|
|
25
|
+
| Passphrase-derived key, **never stored** (Argon2id) | ✗ (GPG keys) | ✅ |
|
|
26
|
+
| Per-file + per-block integrity hashes | partial | ✅ |
|
|
27
|
+
| CLI · admin-UI download · SFTP | CLI · remote storages | ✅ |
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install django-dbs # core
|
|
33
|
+
pip install "django-dbs[ssh]" # + SFTP transport (paramiko)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add the app:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
INSTALLED_APPS = [..., "dbs"]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
### 1. (Optional) declare what to back up
|
|
45
|
+
|
|
46
|
+
By default DBS auto-discovers every model, treats `FileField`/`ImageField` as
|
|
47
|
+
files, and preserves relations. Register a model only to override the defaults —
|
|
48
|
+
in a `dbs.py` module inside your app (auto-discovered like `admin.py`):
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# myapp/dbs.py
|
|
52
|
+
from dbs import backup_registry, FieldType, ModelBackup
|
|
53
|
+
from .models import Invoice
|
|
54
|
+
|
|
55
|
+
@backup_registry.register(Invoice)
|
|
56
|
+
class InvoiceBackup(ModelBackup):
|
|
57
|
+
overrides = {
|
|
58
|
+
"scanned_pdf_path": FieldType.FILE_PATH, # CharField holding a path -> embed the file
|
|
59
|
+
"render_cache": FieldType.EXCLUDE, # don't back this column up
|
|
60
|
+
}
|
|
61
|
+
file_roots = ["/srv/myapp/uploads"] # extra non-model file trees
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`FieldType` values: `VALUE` (default), `FILE` (embed a FileField's bytes),
|
|
65
|
+
`FILE_PATH` (a string column whose path's file is embedded), `EXCLUDE`.
|
|
66
|
+
|
|
67
|
+
### 2. Back up / restore / validate from the CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python manage.py dbs_backup backup.dbs # prompts for a passphrase
|
|
71
|
+
python manage.py dbs_validate backup.dbs # structural check, no passphrase
|
|
72
|
+
python manage.py dbs_validate backup.dbs -p secret # + verify decryption
|
|
73
|
+
python manage.py dbs_restore backup.dbs # restore rows + files
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The passphrase comes from `--passphrase`, then `$DBS_PASSPHRASE`, then a prompt.
|
|
77
|
+
|
|
78
|
+
### 3. Download / upload from the admin UI
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# urls.py
|
|
82
|
+
urlpatterns += [path("dbs/", include("dbs.contrib.urls"))]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Superusers can then visit `/dbs/backup/` to download an encrypted backup and
|
|
86
|
+
`/dbs/restore/` to upload one. The passphrase is entered in the form and never
|
|
87
|
+
stored server-side.
|
|
88
|
+
|
|
89
|
+
### 4. Ship a backup to another server (SFTP)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# settings.py — profiles reference a key by path; no secrets embedded
|
|
93
|
+
DBS_SSH_TARGETS = {
|
|
94
|
+
"offsite": {
|
|
95
|
+
"host": "backups.example.com", "username": "deploy",
|
|
96
|
+
"key_filename": "/home/deploy/.ssh/id_ed25519",
|
|
97
|
+
"remote_dir": "/var/backups/myproject",
|
|
98
|
+
"known_hosts": "/home/deploy/.ssh/known_hosts",
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from dbs import create_backup
|
|
105
|
+
from dbs.transports import SSHTarget, push_backup
|
|
106
|
+
|
|
107
|
+
data = create_backup("my passphrase")
|
|
108
|
+
push_backup(data, "backup.dbs", SSHTarget.from_settings("offsite"))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Python API
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from dbs import create_backup, restore_backup, validate_backup
|
|
115
|
+
|
|
116
|
+
blob = create_backup("passphrase", output="backup.dbs")
|
|
117
|
+
report = validate_backup(blob, "passphrase") # report.ok / report.summary()
|
|
118
|
+
result = restore_backup(blob, "passphrase") # result.healed is True if it repaired corruption
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## How it heals
|
|
122
|
+
|
|
123
|
+
On write, the encrypted stream is split into blocks; each block gets a BLAKE2b
|
|
124
|
+
hash and a layer of Reed-Solomon parity, and the whole stream is stored **twice**
|
|
125
|
+
(plus the header and manifest are stored twice). On read, each block is taken
|
|
126
|
+
from whichever copy verifies; sparse bit-flips are corrected in place by
|
|
127
|
+
Reed-Solomon even when *both* copies are hit. Every recovered block is checked
|
|
128
|
+
against its stored hash, so a mis-correction can never slip through — and a
|
|
129
|
+
freshly written backup is re-read and verified end-to-end (**verify-after-write**)
|
|
130
|
+
before the command reports success.
|
|
131
|
+
|
|
132
|
+
What it can recover from: whole-block loss in one copy, and sparse byte errors
|
|
133
|
+
(up to the parity budget, ~8 bytes per 255-byte codeword by default) in both
|
|
134
|
+
copies. What it cannot: a block destroyed *beyond* the parity budget in **both**
|
|
135
|
+
copies — DBS then refuses to restore and reports exactly which blocks failed,
|
|
136
|
+
rather than producing silently wrong data.
|
|
137
|
+
|
|
138
|
+
## Security model
|
|
139
|
+
|
|
140
|
+
* **Argon2id** derives a key from your passphrase (memory-hard ⇒ brute-force
|
|
141
|
+
resistant). Raise `KDFParams` cost for more resistance.
|
|
142
|
+
* **Envelope encryption**: a random data key encrypts the payload with
|
|
143
|
+
**AES-256-GCM**; that data key is wrapped by the passphrase-derived key. The
|
|
144
|
+
file stores only the salt, Argon2 parameters and the wrapped key — **never the
|
|
145
|
+
passphrase and never the raw data key**.
|
|
146
|
+
* A wrong passphrase fails the GCM tag check and is reported as such; it can
|
|
147
|
+
never yield partial/garbage data.
|
|
148
|
+
|
|
149
|
+
## Settings reference
|
|
150
|
+
|
|
151
|
+
| Setting | Purpose |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `DBS_EXCLUDE_MODELS` | `["app.Model", ...]` to skip (defaults skip contenttypes, permissions, admin log, sessions). |
|
|
154
|
+
| `DBS_FILE_ROOTS` | Extra directories embedded in every backup. |
|
|
155
|
+
| `DBS_SSH_TARGETS` | Named SFTP connection profiles. |
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install -e ".[dev]"
|
|
161
|
+
pytest
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""DBS -- Django Backup Solution.
|
|
2
|
+
|
|
3
|
+
A redundant, self-healing, encrypted, single-file backup for Django projects.
|
|
4
|
+
|
|
5
|
+
Public API::
|
|
6
|
+
|
|
7
|
+
from dbs import backup_registry, FieldType, ModelBackup
|
|
8
|
+
from dbs import create_backup, restore_backup, validate_backup
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .registry import BackupRegistry, FieldType, ModelBackup, backup_registry
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"backup_registry",
|
|
17
|
+
"BackupRegistry",
|
|
18
|
+
"FieldType",
|
|
19
|
+
"ModelBackup",
|
|
20
|
+
"create_backup",
|
|
21
|
+
"restore_backup",
|
|
22
|
+
"validate_backup",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __getattr__(name):
|
|
28
|
+
# Lazily expose the engine entry points so importing ``dbs`` never forces the
|
|
29
|
+
# Django app registry to be ready (the registry/FieldType imports above don't).
|
|
30
|
+
if name in {"create_backup", "restore_backup", "validate_backup"}:
|
|
31
|
+
from . import engine
|
|
32
|
+
|
|
33
|
+
return getattr(engine, name)
|
|
34
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared helpers for the management commands (passphrase resolution, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from django.core.management.base import CommandError
|
|
9
|
+
|
|
10
|
+
ENV_VAR = "DBS_PASSPHRASE"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_passphrase(option_value: str | None, *, confirm: bool = False) -> str:
|
|
14
|
+
"""Resolve a passphrase from the CLI option, the environment, or a prompt.
|
|
15
|
+
|
|
16
|
+
Order: explicit ``--passphrase`` > ``$DBS_PASSPHRASE`` > interactive prompt.
|
|
17
|
+
When ``confirm`` is set (backups), an interactive prompt asks twice.
|
|
18
|
+
"""
|
|
19
|
+
if option_value:
|
|
20
|
+
return option_value
|
|
21
|
+
env_value = os.environ.get(ENV_VAR)
|
|
22
|
+
if env_value:
|
|
23
|
+
return env_value
|
|
24
|
+
passphrase = getpass.getpass("DBS passphrase: ")
|
|
25
|
+
if not passphrase:
|
|
26
|
+
raise CommandError("A passphrase is required.")
|
|
27
|
+
if confirm:
|
|
28
|
+
again = getpass.getpass("Confirm passphrase: ")
|
|
29
|
+
if again != passphrase:
|
|
30
|
+
raise CommandError("Passphrases did not match.")
|
|
31
|
+
return passphrase
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Django app configuration for DBS."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DbsConfig(AppConfig):
|
|
7
|
+
name = "dbs"
|
|
8
|
+
verbose_name = "Django Backup Solution"
|
|
9
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
# Auto-discover ``dbs.py`` modules in installed apps, mirroring how the
|
|
13
|
+
# admin discovers ``admin.py``. This lets projects register models via
|
|
14
|
+
# ``@backup_registry.register(...)`` without an explicit import.
|
|
15
|
+
from django.utils.module_loading import autodiscover_modules
|
|
16
|
+
|
|
17
|
+
autodiscover_modules("dbs")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The container layer: block framing, redundancy, Reed-Solomon and file format.
|
|
2
|
+
|
|
3
|
+
This is the part that makes the backup *self-healing*. The encrypted payload is
|
|
4
|
+
split into fixed-size blocks; every block carries its own hash and a layer of
|
|
5
|
+
Reed-Solomon parity, and the whole encrypted stream is written twice. On read,
|
|
6
|
+
each block is recovered from whichever copy verifies, with Reed-Solomon
|
|
7
|
+
correcting the kind of sparse bit-flips a non-ECC RAM stick produces.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .blocks import (
|
|
11
|
+
DEFAULT_BLOCK_SIZE,
|
|
12
|
+
DEFAULT_NSYM,
|
|
13
|
+
BlockPlan,
|
|
14
|
+
RepairReport,
|
|
15
|
+
encode_copy,
|
|
16
|
+
repair_copies,
|
|
17
|
+
)
|
|
18
|
+
from .format import read_container, write_container
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DEFAULT_BLOCK_SIZE",
|
|
22
|
+
"DEFAULT_NSYM",
|
|
23
|
+
"BlockPlan",
|
|
24
|
+
"RepairReport",
|
|
25
|
+
"encode_copy",
|
|
26
|
+
"repair_copies",
|
|
27
|
+
"read_container",
|
|
28
|
+
"write_container",
|
|
29
|
+
]
|