edgevault 0.5.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.
- edgevault-0.5.0/.gitignore +12 -0
- edgevault-0.5.0/CHANGELOG.md +190 -0
- edgevault-0.5.0/LICENSE +21 -0
- edgevault-0.5.0/PKG-INFO +279 -0
- edgevault-0.5.0/README.md +251 -0
- edgevault-0.5.0/edgevault/__init__.py +21 -0
- edgevault-0.5.0/edgevault/__main__.py +3 -0
- edgevault-0.5.0/edgevault/age.py +268 -0
- edgevault-0.5.0/edgevault/cli.py +311 -0
- edgevault-0.5.0/edgevault/config.py +658 -0
- edgevault-0.5.0/edgevault/crypto.py +58 -0
- edgevault-0.5.0/edgevault/destinations.py +384 -0
- edgevault-0.5.0/edgevault/errors.py +9 -0
- edgevault-0.5.0/edgevault/notify.py +329 -0
- edgevault-0.5.0/edgevault/pull.py +80 -0
- edgevault-0.5.0/edgevault/py.typed +0 -0
- edgevault-0.5.0/edgevault/report.py +165 -0
- edgevault-0.5.0/edgevault/runner.py +187 -0
- edgevault-0.5.0/edgevault/scaffold.py +357 -0
- edgevault-0.5.0/edgevault/schedule.py +18 -0
- edgevault-0.5.0/edgevault/sources/__init__.py +11 -0
- edgevault-0.5.0/edgevault/sources/archive.py +131 -0
- edgevault-0.5.0/edgevault/sources/postgres.py +274 -0
- edgevault-0.5.0/edgevault/sources/sqlite.py +69 -0
- edgevault-0.5.0/edgevault/store.py +247 -0
- edgevault-0.5.0/edgevault/tools.py +177 -0
- edgevault-0.5.0/edgevault/vault.py +415 -0
- edgevault-0.5.0/pyproject.toml +64 -0
- edgevault-0.5.0/tests/conftest.py +25 -0
- edgevault-0.5.0/tests/test_cli.py +152 -0
- edgevault-0.5.0/tests/test_config.py +121 -0
- edgevault-0.5.0/tests/test_notify.py +118 -0
- edgevault-0.5.0/tests/test_proxy.py +219 -0
- edgevault-0.5.0/tests/test_runner.py +286 -0
- edgevault-0.5.0/tests/test_sources.py +275 -0
- edgevault-0.5.0/tests/test_store.py +63 -0
- edgevault-0.5.0/tests/test_vault.py +337 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Изменения
|
|
2
|
+
|
|
3
|
+
Версии — по [semver](https://semver.org/lang/ru/): `МАЖОР.МИНОР.ПАТЧ`.
|
|
4
|
+
|
|
5
|
+
- **ПАТЧ** (0.2.0 → 0.2.1) — исправления. Конфиг не меняется, обновляться можно сразу.
|
|
6
|
+
- **МИНОР** (0.2 → 0.3) — новые возможности. Старые конфиги работают как раньше.
|
|
7
|
+
Пока версия 0.x, минор может менять поведение — это записано здесь в «Внимание».
|
|
8
|
+
- **МАЖОР** (0.x → 1.0) — несовместимые изменения конфига; что поправить — здесь.
|
|
9
|
+
|
|
10
|
+
Версия задаётся в одном месте — `edgevault/__init__.py`, руками её не меняют.
|
|
11
|
+
Пока идёт работа, изменения пишут сюда в раздел «Не выпущено». Выпуск — одна
|
|
12
|
+
команда: `scripts/release.sh X.Y.Z`. Она проверит всё, поставит версию и дату,
|
|
13
|
+
сделает коммит и тег и запушит; CI соберёт образ `ghcr.io/vasiliynemix/edgevault`
|
|
14
|
+
с тегами `X.Y.Z`, `X.Y` и `latest`. В проектах указывайте `X.Y`: исправления
|
|
15
|
+
приходят сами, новые возможности — когда решите.
|
|
16
|
+
|
|
17
|
+
## 0.5.0 — 2026-11-07
|
|
18
|
+
|
|
19
|
+
### Прокси для уведомлений
|
|
20
|
+
|
|
21
|
+
- `Telegram(..., proxy="http://vpn:1080")` и `Webhook(url, proxy=...)`, в
|
|
22
|
+
edgevault.toml — `telegram_proxy` и `webhook_proxy` в `[notify]`. Прокси
|
|
23
|
+
только для сообщений: окружение (`HTTPS_PROXY`) при нём не учитывается, и
|
|
24
|
+
бэкапы через него не идут. Без него — как раньше.
|
|
25
|
+
- Только http(s): socks, адрес без хоста или порта — ошибка конфига сразу.
|
|
26
|
+
В `Vault` плохой прокси выключает только этот канал, бэкап работает, а
|
|
27
|
+
проблема видна в `check`.
|
|
28
|
+
- Пароль прокси и токен бота заменяются на `***` в журналах, `check`, `repr` и
|
|
29
|
+
тексте ошибок. `test-notify` показывает, через какой прокси ушло сообщение.
|
|
30
|
+
|
|
31
|
+
### PyPI
|
|
32
|
+
|
|
33
|
+
- Пакет публикуется на PyPI: `uv add edgevault`, `pip install edgevault` —
|
|
34
|
+
без SSH-ключей, `docker build` ставит его как обычную зависимость. CI
|
|
35
|
+
выкладывает его по тегу (Trusted Publishing, без токенов в репозитории).
|
|
36
|
+
- Лицензия MIT.
|
|
37
|
+
|
|
38
|
+
## 0.4.0 — 2026-11-07
|
|
39
|
+
|
|
40
|
+
### Sync и async проекты
|
|
41
|
+
|
|
42
|
+
- `start()` возвращается сразу: проверка перед стартом (сеть, pg_dump) теперь в
|
|
43
|
+
фоновом потоке. Раньше в async-приложении она держала цикл событий на старте.
|
|
44
|
+
- `await vault.astop()`, `async with vault:` — остановка не блокирует цикл событий.
|
|
45
|
+
- Поток бэкапа — с пониженным приоритетом (nice 10 на поток, pg_dump наследует).
|
|
46
|
+
- `run_forever()` не из главного потока не падает на установке сигнала.
|
|
47
|
+
|
|
48
|
+
### Не ломает приложение
|
|
49
|
+
|
|
50
|
+
- Ошибка в значениях из окружения (нет ключа, опечатка, неверное расписание)
|
|
51
|
+
больше не бросает исключение при создании `Vault`: бэкап выключается и
|
|
52
|
+
сообщает причину. Ошибки типов в коде — по-прежнему исключение сразу.
|
|
53
|
+
- `Vault(..., enabled=False)` — выключить явно (dev, тесты), тихо.
|
|
54
|
+
- `run_forever()` с выключенным бэкапом не выходит, а ждёт: иначе restart:
|
|
55
|
+
always давал бы перезапуски и сообщения по кругу.
|
|
56
|
+
|
|
57
|
+
## 0.3.1 — 2026-11-07
|
|
58
|
+
|
|
59
|
+
### Исправлено
|
|
60
|
+
|
|
61
|
+
- Выпуск 0.3.0 не прошёл CI (форматирование примеров в README), образ 0.3.0 не
|
|
62
|
+
собран — берите 0.3.1. `scripts/release.sh` теперь останавливается на любой
|
|
63
|
+
проваленной проверке: раньше цепочка через `&&` её пропускала.
|
|
64
|
+
|
|
65
|
+
## 0.3.0 — 2026-11-07
|
|
66
|
+
|
|
67
|
+
### Как библиотека в проекте
|
|
68
|
+
|
|
69
|
+
- `Vault(...)` в коде проекта, его же settings: `Postgres(settings.DATABASE_URL)`,
|
|
70
|
+
`S3(url)`, `Telegram(token, chat)`. Запуск как удобно: `run_forever()` —
|
|
71
|
+
отдельным сервисом, `start()`/`stop()` — фоном внутри приложения (lifespan
|
|
72
|
+
FastAPI, aiogram), `run()`/`arun()` — один раз, `cli()` — команды
|
|
73
|
+
daemon | run | check | test-notify.
|
|
74
|
+
- Несколько воркеров с одной папкой копий: прогон делает один, остальные молча
|
|
75
|
+
пропускают. Зависший больше часа прогон — сообщение.
|
|
76
|
+
- `Postgres(url)`: адрес из настроек, в том числе `postgresql+asyncpg://`,
|
|
77
|
+
`sslmode`. Пароль не попадает в журналы.
|
|
78
|
+
- `Secrets(env=[...])`: переменные окружения в архив секретов — для контейнера,
|
|
79
|
+
в который `.env` пришёл через env_file, а не файлом.
|
|
80
|
+
|
|
81
|
+
### Без внешних программ
|
|
82
|
+
|
|
83
|
+
- Шифрование age — на Python (`cryptography`): программа age не нужна ни на
|
|
84
|
+
сервере, ни для `keygen`/`decrypt`. Копии совместимы с `age -d` в обе стороны.
|
|
85
|
+
- `S3` встроен (подпись SigV4): Selectel, Yandex Object Storage, Timeweb, AWS,
|
|
86
|
+
MinIO — без rclone. `Folder(path)` — тоже. rclone нужен только для Google/
|
|
87
|
+
Яндекс Диска и SFTP.
|
|
88
|
+
- В edgevault.toml места `type = "s3"` и `type = "local"` теперь обслуживаются
|
|
89
|
+
встроенно; с нестандартными ключами (storage_class…) — по-прежнему rclone.
|
|
90
|
+
|
|
91
|
+
### Проверка до старта
|
|
92
|
+
|
|
93
|
+
- `check` и запуск `start()`/`daemon`: каких программ нет (с командой установки
|
|
94
|
+
под систему образа: apt, apk, brew, или строкой для Dockerfile), подходит ли
|
|
95
|
+
версия pg_dump к серверу, пускает ли S3/папка/rclone. В журнал и сообщением.
|
|
96
|
+
|
|
97
|
+
### Внимание при обновлении с 0.2
|
|
98
|
+
|
|
99
|
+
- Программа age больше не используется; в образе её нет.
|
|
100
|
+
- S3 из edgevault.toml идёт встроенным клиентом: удаление старых копий — по дате
|
|
101
|
+
в пути (`daily/ГГГГ-ММ-ДД/`), а не по времени файла. Раскладка та же.
|
|
102
|
+
- Открытый ключ проверяется по контрольной сумме: ключ с опечаткой, который
|
|
103
|
+
раньше проходил, теперь — ошибка сразу.
|
|
104
|
+
|
|
105
|
+
## 0.2.1 — 2026-11-07
|
|
106
|
+
|
|
107
|
+
### Исправлено
|
|
108
|
+
|
|
109
|
+
- `init` в папке из нескольких сервисов (compose-файлы в `backend/`, `admin/`…)
|
|
110
|
+
не находил Postgres. Теперь ищет compose-файлы и в папках сервисов на уровень
|
|
111
|
+
ниже, включая `docker-compose.prod.yml`. Включает только проекты, база которых
|
|
112
|
+
запущена на этой машине, остальные пишет закомментированными.
|
|
113
|
+
- Сервис Postgres без своего `image:` (только переопределения в prod-файле)
|
|
114
|
+
тоже узнаётся по имени.
|
|
115
|
+
- `env_file`, которого нет, — не ошибка, если переменные есть в окружении. Если
|
|
116
|
+
их нет, ошибка говорит, какой файл создать.
|
|
117
|
+
- `keygen` без программы age сразу говорит, как её поставить.
|
|
118
|
+
|
|
119
|
+
## 0.2.0 — 2026-11-07
|
|
120
|
+
|
|
121
|
+
### Подключение и запуск
|
|
122
|
+
|
|
123
|
+
- `edgevault init` — смотрит на папку проекта (compose-файл, SQLite, `.env`,
|
|
124
|
+
`config.py`) и пишет готовый конфиг, а также сервис для `docker-compose.yml`.
|
|
125
|
+
- `edgevault test-notify` — проверочное сообщение в настроенные чаты.
|
|
126
|
+
- `edgevault systemd [--install]` — таймер systemd по расписанию из конфига,
|
|
127
|
+
с низким приоритетом процессора и диска.
|
|
128
|
+
- Готовый образ `ghcr.io/vasiliynemix/edgevault:0.2` (собирается CI по тегу).
|
|
129
|
+
- `check` показывает, куда придут сообщения и как прошёл последний прогон.
|
|
130
|
+
|
|
131
|
+
### Конфиг
|
|
132
|
+
|
|
133
|
+
- Коротко строками: `postgres = ["app-db"]`, `sqlite = "data/*.db"`,
|
|
134
|
+
`files = ["uploads"]`, `secrets = true`.
|
|
135
|
+
- `[[postgres]] discover = true` + `compose_project = "имя"` — все базы своего
|
|
136
|
+
проекта Docker Compose и ничьи больше.
|
|
137
|
+
- `min_version` — конфиг сам требует нужную версию программы.
|
|
138
|
+
- Неизвестный ключ или ключ не в своём разделе — ошибка с объяснением, а не
|
|
139
|
+
молча выключенная часть бэкапа.
|
|
140
|
+
- Сроки хранения проверяются: 0 дней удалил бы свежие копии.
|
|
141
|
+
|
|
142
|
+
### Сообщения
|
|
143
|
+
|
|
144
|
+
- Проект, сервер, время, длительность; что не так — по пунктам с текстом
|
|
145
|
+
ошибки и подсказкой, что делать; сколько копий сделано и куда дошли.
|
|
146
|
+
- Telegram с разметкой, несколько чатов, темы форумов (`telegram_thread`).
|
|
147
|
+
- Webhook — JSON с готовым `text` и всеми полями.
|
|
148
|
+
- «Снова в порядке» после сбоев (`on_recovery`), успешные прогоны без звука
|
|
149
|
+
(`on_success`), один и тот же сбой не чаще раза в N часов (`remind_hours`).
|
|
150
|
+
- Имя сервера из Docker — по смонтированному `/etc/hostname`.
|
|
151
|
+
- Падение самой программы при `run` из cron теперь тоже приходит сообщением.
|
|
152
|
+
- Нет нужной программы (age, rclone, pg_dump, psql, docker) — по пункту на каждую:
|
|
153
|
+
зачем нужна и как поставить; в Docker — совет про образ. Без age открытые
|
|
154
|
+
дампы не делаются вовсе. В контейнере базы нет psql/pg_dump — своя подсказка.
|
|
155
|
+
|
|
156
|
+
### Безопасность баз
|
|
157
|
+
|
|
158
|
+
- Все сессии Postgres — только на чтение (`default_transaction_read_only`):
|
|
159
|
+
запись сервер отклонит сам.
|
|
160
|
+
- pg_dump не ждёт блокировок дольше 2 минут: идёт миграция — дамп отступает,
|
|
161
|
+
а не мешает ей; работает с пониженным приоритетом.
|
|
162
|
+
- SQLite всегда открывается только на чтение, и путь экранируется.
|
|
163
|
+
|
|
164
|
+
### Исправлено
|
|
165
|
+
|
|
166
|
+
- Контейнер официального образа без `POSTGRES_USER` снимался как root и падал.
|
|
167
|
+
- База `postgres` с данными молча пропускалась.
|
|
168
|
+
- При `discover` сбой одного контейнера отменял бэкап всех; PostgREST и
|
|
169
|
+
postgres-exporter принимались за базы.
|
|
170
|
+
- SQLite с `#`, `?` в имени давал копию новой пустой базы без ошибки.
|
|
171
|
+
- Базы с пробелом в имени не снимались.
|
|
172
|
+
- Открытые дампы были видны другим пользователям машины (теперь `0600`/`0700`).
|
|
173
|
+
- `pull` мог скачать открытый дамп из рабочей папки сервера.
|
|
174
|
+
- Остатки прерванного прогона не убирались; два прогона сразу мешали друг другу.
|
|
175
|
+
- Месячная копия терялась, если отправка первого числа не удалась.
|
|
176
|
+
- `rclone rmdirs` чистил пустые папки по всему месту хранения, а не только свои.
|
|
177
|
+
- Одноимённые копии из разных источников затирали друг друга.
|
|
178
|
+
- `decrypt` мог записать поверх копии или готового файла.
|
|
179
|
+
|
|
180
|
+
### Внимание при обновлении с 0.1
|
|
181
|
+
|
|
182
|
+
- В секреты по умолчанию добавлены `.env.*` и `config.py` (корень и папки на
|
|
183
|
+
уровень ниже). Образцы `.env.example` и подобные не берутся.
|
|
184
|
+
- Ключ не в своём разделе теперь ошибка: запустите `edgevault check`.
|
|
185
|
+
- Имена копий Postgres без изменений; у баз со спецсимволами в имени они
|
|
186
|
+
заменяются на `_`.
|
|
187
|
+
|
|
188
|
+
## 0.1.0
|
|
189
|
+
|
|
190
|
+
Первая версия: Postgres, SQLite, папки, секреты, age, rclone, pull.
|
edgevault-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vasiliynemix
|
|
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.
|
edgevault-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: edgevault
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Encrypted backups for your project: Postgres, SQLite, files and secrets to S3, Google Drive, Yandex Disk, SFTP — with Telegram alerts
|
|
5
|
+
Project-URL: Homepage, https://github.com/Vasiliynemix/EdgeVault
|
|
6
|
+
Project-URL: Changelog, https://github.com/Vasiliynemix/EdgeVault/blob/main/CHANGELOG.md
|
|
7
|
+
Author: Vasiliynemix
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: age,backup,encryption,pg_dump,postgres,s3,sqlite,telegram
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Natural Language :: Russian
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# edgevault
|
|
30
|
+
|
|
31
|
+
*Encrypted backups for Python projects: Postgres, SQLite, files and secrets → S3,
|
|
32
|
+
Google Drive, Yandex Disk, SFTP, with Telegram alerts. Docs are in Russian.*
|
|
33
|
+
|
|
34
|
+
Зашифрованные бэкапы для проекта: Postgres, SQLite, папки, секреты. Подключается
|
|
35
|
+
к бэкенду как обычная библиотека, настраивается его же settings, запускается
|
|
36
|
+
вместе с проектом. О сбое приходит сообщение в Telegram: какой проект, какой
|
|
37
|
+
сервер, что сломалось и что делать.
|
|
38
|
+
|
|
39
|
+
Базы edgevault не меняет: подключения к Postgres только на чтение (запись
|
|
40
|
+
отклонит сам сервер), SQLite открывается только на чтение, удаляет edgevault
|
|
41
|
+
только свои старые копии.
|
|
42
|
+
|
|
43
|
+
## В проект за три шага
|
|
44
|
+
|
|
45
|
+
**1. Зависимость**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv add edgevault # или pip install edgevault
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**2. Файл `app/backup.py`** — всё, что нужно знать о бэкапе проекта:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from edgevault import Postgres, S3, Secrets, Telegram, Vault
|
|
55
|
+
|
|
56
|
+
from app.config import settings
|
|
57
|
+
|
|
58
|
+
vault = Vault(
|
|
59
|
+
"my-project",
|
|
60
|
+
recipients=settings.BACKUP_AGE_KEY, # открытый ключ age1…
|
|
61
|
+
sources=[
|
|
62
|
+
Postgres(settings.DATABASE_URL), # можно postgresql+asyncpg://…
|
|
63
|
+
Secrets(env=["SECRET_KEY", "FERNET_KEY"]), # ключи, без которых дамп не прочитать
|
|
64
|
+
],
|
|
65
|
+
targets=[S3(settings.BACKUP_S3_URL)], # https://KEY:SECRET@host/bucket/папка
|
|
66
|
+
notify=Telegram(settings.BOT_TOKEN, settings.ADMIN_CHAT_ID),
|
|
67
|
+
schedule=["04:00"],
|
|
68
|
+
store="/backups",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
vault.cli() # python -m app.backup [daemon|run|check|test-notify]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**3. Запуск** — как удобно:
|
|
76
|
+
|
|
77
|
+
| Как | Что написать |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Отдельный сервис рядом (рекомендую) | в `docker-compose.yml` — сервис ниже |
|
|
80
|
+
| Внутри приложения, фоном | `vault.start()` при старте, `vault.stop()` при остановке |
|
|
81
|
+
| Один раз | `python -m app.backup run`, или `vault.run()` из своего кода |
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
backup:
|
|
85
|
+
build: . # тот же образ, что у бэкенда
|
|
86
|
+
command: python -m app.backup
|
|
87
|
+
env_file: .env
|
|
88
|
+
volumes: ["backups:/backups"]
|
|
89
|
+
restart: unless-stopped
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Внутри приложения — например, FastAPI:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def lifespan(app):
|
|
97
|
+
vault.start() # расписание в своём потоке, приложению не мешает
|
|
98
|
+
yield
|
|
99
|
+
vault.stop()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Несколько воркеров uvicorn с одной папкой копий — не страшно: прогон делает
|
|
103
|
+
один, остальные молча пропускают. Но если API масштабируется на несколько
|
|
104
|
+
машин, берите отдельный сервис.
|
|
105
|
+
|
|
106
|
+
### Sync или async — без разницы, и проект он не ломает
|
|
107
|
+
|
|
108
|
+
- `start()` возвращается сразу. Проверки, сеть, pg_dump — в своём потоке с
|
|
109
|
+
пониженным приоритетом: цикл событий asyncio и потоки приложения не ждут.
|
|
110
|
+
- Async-код: `async with vault:` или `vault.start()` … `await vault.astop()`,
|
|
111
|
+
разовый прогон — `await vault.arun()`. Sync-код (Django, Flask, скрипты):
|
|
112
|
+
`with vault:` или `start()`/`stop()`, разовый — `vault.run()`.
|
|
113
|
+
- Ошибка в окружении (пустой `BACKUP_AGE_KEY`, опечатка в ключе) не роняет
|
|
114
|
+
импорт и старт приложения: бэкап выключается и говорит почему — в журнал,
|
|
115
|
+
в `check` и в чат. Выключить явно, например в dev: `Vault(..., enabled=settings.IS_PROD)`.
|
|
116
|
+
- `run()` и прогоны по расписанию не бросают исключений: всё — в отчёт и в чат.
|
|
117
|
+
- Запускайте `start()` в процессе воркера (lifespan, `on_startup`), а не при
|
|
118
|
+
импорте: с `gunicorn --preload` поток, запущенный до fork, в воркеры не попадёт.
|
|
119
|
+
|
|
120
|
+
## Ключ — один раз, на своём компьютере
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv tool install edgevault
|
|
124
|
+
edgevault keygen # age-key.txt — в менеджер паролей; age1… — в BACKUP_AGE_KEY
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
На сервер кладут только открытый `age1…`: украденная копия без закрытого ключа
|
|
128
|
+
бесполезна. Программа `age` не нужна — шифрование встроено, но копии
|
|
129
|
+
расшифровывает и обычный `age -d`.
|
|
130
|
+
|
|
131
|
+
## До старта скажет, чего не хватает
|
|
132
|
+
|
|
133
|
+
`python -m app.backup check` (и сам `start()`/`daemon` при запуске — в журнал и
|
|
134
|
+
сообщением) проверяет всё, что можно проверить, ничего не делая:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
✗ Программа pg_dump: не установлена, а нужна, чтобы снимать Postgres по адресу
|
|
138
|
+
→ Добавьте в Dockerfile образа: RUN apt-get update && apt-get install -y --no-install-recommends postgresql-client
|
|
139
|
+
✗ Программа pg_dump: версии 17, а сервер Postgres 18: старый pg_dump новую базу не снимет
|
|
140
|
+
✗ Отправка → s3://storage.yandexcloud.net/backups: HTTP 403 SignatureDoesNotMatch …
|
|
141
|
+
→ Ключи S3 неверны или у них нет прав на бакет
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Команда установки — под систему образа (Debian, Alpine) или машины. Внешние
|
|
145
|
+
программы нужны не всегда:
|
|
146
|
+
|
|
147
|
+
| Что в конфиге | Что нужно поставить |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `Postgres(url)` | `postgresql-client` той же версии, что сервер, или новее |
|
|
150
|
+
| `S3`, `Folder`, `Sqlite`, `Files`, `Secrets`, шифрование | ничего |
|
|
151
|
+
| `Rclone("gdrive:…")` — Google/Яндекс Диск, SFTP | `rclone` |
|
|
152
|
+
| Postgres в соседних контейнерах (`container`, `discover`) | `docker` и смонтированный `docker.sock` |
|
|
153
|
+
|
|
154
|
+
## Что сохранять
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
Postgres(settings.DATABASE_URL) # по адресу; база из адреса
|
|
158
|
+
Postgres(host="db", user="app", password=..., databases=["app", "auth"])
|
|
159
|
+
Postgres(container="app-db") # pg_dump внутри контейнера базы
|
|
160
|
+
Postgres(discover=True, compose_project="app") # все базы своего compose-проекта
|
|
161
|
+
Sqlite("data/*.sqlite3") # согласованная копия, даже если пишут
|
|
162
|
+
Files("uploads", exclude=["cache/*"]) # папка архивом
|
|
163
|
+
Secrets(env=["SECRET_KEY"]) # переменные окружения
|
|
164
|
+
Secrets(root=".", patterns=[".env", "config.py"]) # или файлы
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Куда отправлять
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
S3("https://KEY:SECRET@s3.ru-1.storage.selcloud.ru/bucket/my-project")
|
|
171
|
+
S3(
|
|
172
|
+
endpoint="https://storage.yandexcloud.net",
|
|
173
|
+
bucket="b",
|
|
174
|
+
prefix="app",
|
|
175
|
+
access_key=...,
|
|
176
|
+
secret_key=...,
|
|
177
|
+
region="ru-central1",
|
|
178
|
+
)
|
|
179
|
+
Folder("/mnt/backup-disk/my-project") # другой диск, NFS, volume
|
|
180
|
+
Rclone("gdrive:Backups/my-project", {"type": "drive", "token": settings.GDRIVE_TOKEN})
|
|
181
|
+
Rclone("yadisk:Backups/my-project", {"type": "yandex", "token": settings.YADISK_TOKEN})
|
|
182
|
+
Rclone(
|
|
183
|
+
"box:/backups",
|
|
184
|
+
{
|
|
185
|
+
"type": "sftp",
|
|
186
|
+
"host": "backup.example.ru",
|
|
187
|
+
"user": "b",
|
|
188
|
+
"key_file": "/run/secrets/backup_key",
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Мест можно несколько: не дошло в одно — остальные всё равно получают копию.
|
|
194
|
+
Хотя бы одно — у другого провайдера, не там, где сервер. Без мест копии лежат
|
|
195
|
+
только в `store`; их можно забирать к себе: `edgevault pull`.
|
|
196
|
+
|
|
197
|
+
Токен Google/Яндекс Диска: `edgevault authorize gdrive` (или `yandex`) на своём
|
|
198
|
+
компьютере — откроется вход в браузере. У Google Диска доступ только к файлам,
|
|
199
|
+
которые положил сам edgevault.
|
|
200
|
+
|
|
201
|
+
Хранение: в `store` 3 дня, ежедневные в местах хранения 14 дней, месячные год —
|
|
202
|
+
`keep=Keep(local_days=2, daily_days=30, monthly_days=365)`.
|
|
203
|
+
|
|
204
|
+
## Сообщения
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
🟠 my-project · бэкап с проблемами
|
|
208
|
+
prod-1 · 07.11.2026 04:00
|
|
209
|
+
|
|
210
|
+
Что не так (1):
|
|
211
|
+
• Отправка → s3://storage.yandexcloud.net/backups
|
|
212
|
+
HTTP 403 SignatureDoesNotMatch
|
|
213
|
+
💡 Ключи S3 неверны или у них нет прав на бакет
|
|
214
|
+
|
|
215
|
+
Копии: 2 · 8,9 МБ
|
|
216
|
+
Места хранения: s3://storage.yandexcloud.net/backups ✗
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
🔴 — копий нет, 🟠 — часть не сделана, ✅ — снова в порядке. Параметры `Vault`:
|
|
220
|
+
`on_success` (сообщать и об успехах, без звука), `on_recovery` (по умолчанию да),
|
|
221
|
+
`remind_hours` (тот же сбой — не чаще раза в N часов), `server` (имя сервера в
|
|
222
|
+
сообщении). `Telegram(token, "-100…, @ops", thread=42)` — несколько чатов и тема
|
|
223
|
+
форума; `Webhook(url)` — JSON со всеми полями. Проверить: `python -m app.backup test-notify`.
|
|
224
|
+
|
|
225
|
+
### Через прокси
|
|
226
|
+
|
|
227
|
+
Если с сервера Telegram доступен только через VPN или прокси, задайте его только
|
|
228
|
+
для сообщений, бэкапы через него не пойдут:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
notify = Telegram(settings.BOT_TOKEN, settings.ADMIN_CHAT_ID, proxy="http://vpn:1080")
|
|
232
|
+
Webhook("https://hooks.example/backup", proxy="http://user:pass@proxy:3128")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
В edgevault.toml: `telegram_proxy` и `webhook_proxy` в `[notify]`. Поддерживаются
|
|
236
|
+
только http(s)-прокси, socks — нет: такой адрес будет ошибкой сразу, а не в день
|
|
237
|
+
аварии. Пароль прокси и токен бота в журналах и выводе заменяются на `***`.
|
|
238
|
+
`test-notify` пишет, через какой прокси ушло сообщение.
|
|
239
|
+
|
|
240
|
+
Без `proxy` сообщения идут, как настроена система (`HTTPS_PROXY`, `NO_PROXY`).
|
|
241
|
+
Встроенный S3 и `Folder` прокси из окружения не читают, а rclone читает:
|
|
242
|
+
`HTTPS_PROXY` на весь процесс погонит через прокси и копии в Google/Яндекс Диск.
|
|
243
|
+
|
|
244
|
+
## Восстановление
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
edgevault decrypt app__app.dump.age -k age-key.txt # → app__app.dump
|
|
248
|
+
createdb app_restore && pg_restore -d app_restore --no-owner app__app.dump # сначала — в новую базу
|
|
249
|
+
edgevault decrypt secrets.tar.gz.age -k age-key.txt && tar xzf secrets.tar.gz
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Раскладка копий везде одна: `daily/ГГГГ-ММ-ДД/ЧЧММ/<копия>.age`,
|
|
253
|
+
`monthly/ГГГГ-ММ/<копия>.age`.
|
|
254
|
+
|
|
255
|
+
## Без кода: edgevault.toml
|
|
256
|
+
|
|
257
|
+
Для проектов, в код которых лезть не хочется, — отдельная программа и один
|
|
258
|
+
конфиг: `edgevault init` в папке проекта напишет его сам (найдёт Postgres в
|
|
259
|
+
compose-файлах, SQLite, `.env`), справочник — [`examples/edgevault.toml`](examples/edgevault.toml).
|
|
260
|
+
Запуск — `edgevault daemon`, таймером `sudo edgevault systemd --install` или
|
|
261
|
+
образом `ghcr.io/vasiliynemix/edgevault:0.5` рядом с проектом.
|
|
262
|
+
|
|
263
|
+
## Обновление
|
|
264
|
+
|
|
265
|
+
[semver](https://semver.org/lang/ru/), что изменилось — в [CHANGELOG.md](CHANGELOG.md).
|
|
266
|
+
|
|
267
|
+
- в проекте: `uv add edgevault@latest` (или поднять версию в зависимостях) и `uv lock`;
|
|
268
|
+
- программа: `uv tool upgrade edgevault`;
|
|
269
|
+
- образ: тег `0.5` сам указывает на последнее исправление — `docker compose pull`.
|
|
270
|
+
|
|
271
|
+
После обновления — `check`.
|
|
272
|
+
|
|
273
|
+
## Разработка
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
uv sync
|
|
277
|
+
uv run pytest && uv run ruff check . && uv run ruff format --check . && uv run mypy edgevault
|
|
278
|
+
scripts/release.sh 0.5.1 # выпуск: проверки → версия → коммит, тег, push → CI → PyPI
|
|
279
|
+
```
|