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.
Files changed (37) hide show
  1. edgevault-0.5.0/.gitignore +12 -0
  2. edgevault-0.5.0/CHANGELOG.md +190 -0
  3. edgevault-0.5.0/LICENSE +21 -0
  4. edgevault-0.5.0/PKG-INFO +279 -0
  5. edgevault-0.5.0/README.md +251 -0
  6. edgevault-0.5.0/edgevault/__init__.py +21 -0
  7. edgevault-0.5.0/edgevault/__main__.py +3 -0
  8. edgevault-0.5.0/edgevault/age.py +268 -0
  9. edgevault-0.5.0/edgevault/cli.py +311 -0
  10. edgevault-0.5.0/edgevault/config.py +658 -0
  11. edgevault-0.5.0/edgevault/crypto.py +58 -0
  12. edgevault-0.5.0/edgevault/destinations.py +384 -0
  13. edgevault-0.5.0/edgevault/errors.py +9 -0
  14. edgevault-0.5.0/edgevault/notify.py +329 -0
  15. edgevault-0.5.0/edgevault/pull.py +80 -0
  16. edgevault-0.5.0/edgevault/py.typed +0 -0
  17. edgevault-0.5.0/edgevault/report.py +165 -0
  18. edgevault-0.5.0/edgevault/runner.py +187 -0
  19. edgevault-0.5.0/edgevault/scaffold.py +357 -0
  20. edgevault-0.5.0/edgevault/schedule.py +18 -0
  21. edgevault-0.5.0/edgevault/sources/__init__.py +11 -0
  22. edgevault-0.5.0/edgevault/sources/archive.py +131 -0
  23. edgevault-0.5.0/edgevault/sources/postgres.py +274 -0
  24. edgevault-0.5.0/edgevault/sources/sqlite.py +69 -0
  25. edgevault-0.5.0/edgevault/store.py +247 -0
  26. edgevault-0.5.0/edgevault/tools.py +177 -0
  27. edgevault-0.5.0/edgevault/vault.py +415 -0
  28. edgevault-0.5.0/pyproject.toml +64 -0
  29. edgevault-0.5.0/tests/conftest.py +25 -0
  30. edgevault-0.5.0/tests/test_cli.py +152 -0
  31. edgevault-0.5.0/tests/test_config.py +121 -0
  32. edgevault-0.5.0/tests/test_notify.py +118 -0
  33. edgevault-0.5.0/tests/test_proxy.py +219 -0
  34. edgevault-0.5.0/tests/test_runner.py +286 -0
  35. edgevault-0.5.0/tests/test_sources.py +275 -0
  36. edgevault-0.5.0/tests/test_store.py +63 -0
  37. edgevault-0.5.0/tests/test_vault.py +337 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ .venv/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .mypy_cache/
6
+ dist/
7
+ backups/
8
+ *.age
9
+ age-key.txt
10
+ .env
11
+ edgevault.toml
12
+ !examples/*.toml
@@ -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.
@@ -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.
@@ -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
+ ```