# pg-stand-sync Перенос базы PostgreSQL со стенда на локальный сервер: `pg_dump -Fc` → пересоздание локальной БД → `pg_restore -j`. Ядро — stdlib Python 3.10+; TUI-пульт требует `textual` (см. `requirements.txt`), без него работает прежний текстовый режим. Утилиты берутся из поставки pgAdmin 4. Работает в двух режимах: пульт (запуск без аргументов) и команды CLI. ## Быстрый старт ```bash copy config.example.json config.json ``` ```powershell .\run.ps1 -Setup ``` Заполнить `source` (стенд) и `target` (локальная PG), затем: ```powershell .\run.ps1 ``` `run.ps1` — обёртка запуска: берёт интерпретатор из `.venv` (иначе системный), переключает консоль в UTF-8 и пробрасывает остальные аргументы в скрипт (`.\run.ps1 list`, `.\run.ps1 --dry-run sync zpas`). `-Setup` создаёт окружение и ставит зависимости, `-Tests` прогоняет тесты. Без обёртки всё то же работает через `python pg_stand_sync.py`. Откроется пульт: оба списка сразу на экране, отдельного шага «выбор действия» нет. ``` ┌ pg-stand-sync ─────────────────────── ◆ DRY-RUN ─┐ │ стенд postgres@postgresql.lan → локально :5432 │ ├──────────────────────────┬───────────────────────┤ │ Базы стенда показано 3/41│ Приёмник · localhost │ │ фильтр▸ zpa▌ │ режим DROP+CREATE │ │ ● zpas 1284 MB │ цель как на стенде │ │ ○ zpas_arch 312 MB ├──────────────────────┤ │ ○ zpas_test 88 MB │ Дампы показано 5/12 │ │ отмечено 1 │ ▸ zpas-…0313 1.2 ГБ │ ├──────────────┬────────────┴──────────────────────┤ │ план │ журнал │ │ ✓ дамп 12.4c │ [03:14:07] дамп zpas: "…pg_dump…" │ │ ▸ DROP ⣾ │ pg_dump: сохранение "public…" │ ├──────────────┴───────────────────────────────────┤ │ ⣾ sync zpas · шаг 3/6 · 00:42 ████████░░░ ^X │ └──────────────────────────────────────────────────┘ ``` Фильтр сужает список на каждом набранном символе — Enter для этого не нужен. | Клавиша | Что делает | |---|---| | набор букв | фильтр панели под фокусом; несколько слов — И | | `↑` `↓` `PgUp` `PgDn` | курсор в списке, фокус остаётся в поле фильтра | | `Tab` | следующая панель | | `Enter` | базы → окно параметров дампа с галочкой «поднять локально», дампы → Restore | | `Space` | отметить базу (когда фокус в списке) | | `Ctrl+D` | то же окно с выключённым автоподнятием — снять только дамп | | `Ctrl+A` / `Ctrl+U` | отметить показанные / снять все отметки | | `Esc` | очистить фильтр, затем отметки | | `F4` | журнал во весь экран и обратно (`Esc` — тоже назад) | | `F5` | перечитать оба списка | | `Ctrl+X` | прервать операцию (это kill процессов, а не откат) | | `Ctrl+L` / `Ctrl+S` | очистить журнал / сохранить его в `dumps/session-.log` | | `F1` / `F2` / `F3` | помощь / тема / показывать ли вывод утилит | | `Ctrl+C` | выход | В окне операции: `↑` `↓` (и `Tab`) — переход по полям, `Space` — переключить галочку, `Ctrl+Enter` — начать, `Esc` — отмена. Раскладка клавиатуры значения не имеет: каждый `Ctrl`-хоткей продублирован кириллическим двойником по ЙЦУКЕН (`Ctrl+В` = `Ctrl+D`, `Ctrl+Ч` = `Ctrl+X` и так далее). На старте операции пульт сам разворачивает журнал на весь экран и сам сворачивает его обратно, когда всё закончилось; если развернуть журнал руками (`F4`), пульт больше не трогает раскладку. Раскладка адаптивная: уже 100 колонок — панели встают друг под друга, ниже 32 строк журнал занимает долю экрана вместо фиксированных 20 строк. Списки, журнал и план листаются стрелками и `PgUp`/`PgDn`. Отметки переживают смену фильтра и не сужаются им: в операцию уходят все отмеченные базы, сколько бы их ни показывала панель. Вывод `pg_dump`/`pg_restore` идёт в журнал построчно, пока утилита работает. Sync нескольких баз сразу возможен, только когда `target.database` = `null` (каждая база льётся в одноимённую локальную). С заданным `target.database` пульт откажет: все отмеченные базы легли бы по очереди в одну и ту же локальную базу. ### Текстовый режим Пульт не запускается, если не установлен `textual`, вывод не в терминал, задан `--no-tui` или переменная `PG_STAND_SYNC_NO_TUI`. Тогда открывается прежнее меню, а список выглядит так: | Ввод | Что делает | |---|---| | текст | фильтрует список по подстроке (регистр не важен), применяется по Enter | | номер | выбирает пункт; в Dump можно `2,5,7` — несколько баз | | пусто | сбрасывает фильтр | | `q` | назад в меню | Флаг `--tui` — наоборот: при отсутствии `textual` он завершается ошибкой, а не откатывается молча в текст. `config.json` в `.gitignore` — пароли в репозиторий не попадают. ## CLI ```bash python pg_stand_sync.py list # базы на стенде python pg_stand_sync.py dumps # локальные дампы python pg_stand_sync.py dump zpas # только дамп python pg_stand_sync.py sync zpas --target-db zpas_l # дамп + restore python pg_stand_sync.py restore dumps/zpas-20260820-031332.dump ``` Имя базы (или файла) можно не указывать — тогда откроется тот же список с фильтрацией. | Команда / ключ | Что делает | |---|---| | `sync [DB…]` | dump со стенда + restore локально | | `dump [DB…]` | только снять дампы со стенда (можно несколько баз) | | `restore [FILE]` | залить готовый `.dump` локально, без обращения к стенду | | `list` / `dumps` | базы стенда / файлы дампов | | `menu` | интерактивное меню (то же, что запуск без команды) | | `--target-db NAME` | имя локальной базы (по умолчанию как на стенде) | | `--no-recreate` | не делать DROP/CREATE DATABASE, накатить поверх (`pg_restore --clean --if-exists`) | | `-c, --config PATH` | другой конфиг (несколько стендов — несколько json) | | `--dry-run` | напечатать команды, ничего не выполнять | | `--no-tui` | меню текстом, без Textual (то же делает `PG_STAND_SYNC_NO_TUI=1`) | | `--tui` | только пульт: без `textual` завершиться ошибкой, а не откатываться в текст | Имя базы или файла у CLI-команд без аргумента по-прежнему спрашивается текстовым списком, даже когда `textual` установлен: подсовывать полноэкранный пульт в середину `sync DB` значит ломать pipe-сценарии. ## Конфиг | Поле | Значение по умолчанию | Смысл | |---|---|---| | `pg_bin_dir` | из `PATH` | каталог с `pg_dump.exe`, `pg_restore.exe`, `psql.exe` | | `dump_dir` | `dumps` | куда складывать дампы (относительный путь — от каталога скрипта) | | `keep_dumps` | `5` | сколько последних дампов хранить, `0` — не чистить | | `source.maintenance_database` | `postgres` | база, к которой подключаться для чтения списка баз | | `source.exclude_databases` | `[]` | массив строк: базы, которые не показывать в списках | | `source.password_env` | — | имя переменной окружения с паролем вместо `password` в файле | | `target.database` | `null` | имя локальной базы; `null` — как на стенде | | `target.recreate` | `true` | DROP + CREATE локальной базы перед восстановлением | | `dump.schemas` / `exclude_*` | `[]` | ограничение состава: схемы, таблицы, данные таблиц | | `restore.jobs` | `4` | параллельные воркеры `pg_restore` | | `restore.exit_on_error` | `false` | падать на первой ошибке восстановления | | `post_restore_sql` | `[]` | список SQL, выполняемых в целевой базе после восстановления | ```json "exclude_databases": ["_old", "keycloak", "*_tmp"] ``` Строка без спецсимволов — подстрока (регистр не важен), со `*`, `?` или `[` — маска. Фильтр действует только на списки: базу, названную прямо (`sync zpas_old`), он не прячет. Пароль ищется в три шага: `password` в конфиге → переменная окружения из `password_env` → запрос с терминала (один раз за запуск, ввод скрыт). В неинтерактивном запуске третьего шага нет — либо `password_env`, либо `pgpass.conf`. Пароль всегда уходит в дочерний процесс через `PGPASSWORD`, в командной строке не появляется; если пароля нет вовсе, утилиты вызываются с `--no-password`, чтобы скрипт не подвисал на приглашении. ```powershell $env:STAND_PGPASSWORD = '...' python pg_stand_sync.py sync zpas ``` ## Как это работает ``` config.json │ ├─ pg_dump -Fc ──► dumps/-.dump (стенд, только чтение) │ ├─ psql: pg_terminate_backend → DROP DATABASE → CREATE DATABASE (локально) │ ├─ pg_restore --jobs N --no-owner --no-privileges │ └─ post_restore_sql: анонимизация, правка настроек, GRANT'ы ``` Обезличивание данных стенда, если оно нужно, делается через `post_restore_sql` — уже в локальной базе, чтобы стенд оставался нетронутым. ## Тесты ```bash .venv/Scripts/python.exe -m pytest tests_tui.py -q ``` `tests_tui.py` гоняет пульт headless (`App.run_test()` + `Pilot`): настоящий терминал не открывается, список баз подменяется фикстурой, к `postgresql.lan` никто не ходит. Там же проверяется паритет текстового режима: состав `argv`, текст `StepError`, стриминг вывода, отмена и то, что пароль не попадает в командную строку.