⚠️ ВНИМАНИЕ⚠️ Репозиторий архивирован. Утилита доступна в составе пакета: fs-tools.
Консольная утилита односторонней синхронизации локального каталога с сервером
(ПК → сервер) через внешний rsync поверх SSH. Состав синхронизации задаётся
декларативно в файле .fs-sync.toml в корне каталога. Путь к каталогу передаётся
аргументом командной строки, поэтому утилита пригодна для запуска по расписанию
(cron / systemd timer / Планировщик Windows) без интерактивного диалога.
fs-syncher — тонкая обёртка вокруг rsync: читает и валидирует конфиг, транслирует
правила include/exclude в фильтры rsync (сопоставление путей выполняет сам rsync),
формирует и запускает команду rsync, разбирает её итог, дописывает журнал .fs-log
и при необходимости шлёт веб-хук.
- Зеркалирование каталога на сервер (профиль
[[sync]]): передача новых и изменённых файлов (delta), зеркалирование удалений (--delete). - Offload (профиль
[[backup]]): выгрузка на сервер с последующим локальным удалением или архивированием — только после подтверждённой передачи. - Правила include/exclude (gitignore-подобные фильтры
rsync). - Защита от массового удаления (delete-guard): предварительный расчёт и пороги.
- Безопасность данных: однонаправленность, авто-исключение служебных файлов,
валидация
remote_root, идемпотентность повторных прогонов. - Журнал
.fs-logв общем формате, fire-and-forget веб-хук, несколько профилей в одном конфиге, выборочный запуск.
sync_fs.py точка входа; обёртки sync.sh/.command/.bat (авто-venv + pip)
syncher/ пакет утилиты (публичное API — в __init__.py)
tests/ pytest
examples/ запускаемая песочница (без сети)
.fs-sync.toml конфиг (в корне синхронизируемого каталога; пример — в examples/)
.env.example переменные веб-хука
- Python 3.10+.
- Рантайм-зависимости:
requests>=2.31,python-dotenv>=1.0(разбор TOML — стандартныйtomllibна 3.11+,tomliна 3.10). - Внешний
rsync— обязателен (проверяется на старте).ssh(OpenSSH) — только при SSH-формеremote_root; локальная цель (каталог→каталог) работает безssh.
Обёртки при первом запуске сами готовят .venv и зависимости:
./sync.sh /путь/к/каталогу # Linux/macOS (терминал)
./sync.command /путь/к/каталогу # macOS (двойной клик в Finder)
./sync.bat C:\путь\к\каталогу # Windows (через WSL/cwrsync)Либо вручную:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python sync_fs.py /путь/к/каталогуpython sync_fs.py [КАТАЛОГ] [ФЛАГИ]КАТАЛОГ— корень синхронизации с файлом.fs-sync.toml. Если не задан — текущий рабочий каталог.local_rootпрофилей отсчитывается от этого каталога.
Флаги:
| Флаг | Назначение |
|---|---|
--profile NAME |
Запустить только указанный профиль (повторяемый). |
--all |
Явно запустить все профили (поведение по умолчанию). |
--dry-run |
Показать план без передачи/удаления. Приоритетнее dry_run профиля; выключить dry-run профиля флагом нельзя. |
--force-delete |
Снять защиту от массового удаления (delete-guard). |
--verbose |
Печатать подробный вывод rsync. |
Профили выполняются последовательно; сбой одного не прерывает остальные. На старте
печатаются профили, корень и режим; в конце — отчёт по каждому профилю (передано /
удалено / выгружено / ошибки). Ошибки — в stderr.
| Код | Значение |
|---|---|
0 |
Успех (включая «изменений нет»). |
1 |
Ошибка запуска: нет каталога/конфига, ошибка валидации, нет rsync (или ssh при SSH-цели). |
2 |
rsync или offload завершились ошибкой (передача неполная). |
3 |
Остановлено delete-guard (превышен порог удаления без подтверждения). |
Итоговый код прогона — наихудший среди профилей по шкале 0 < 2 < 3 (код 1 —
ошибка ещё до запуска профилей).
Файл лежит в корне синхронизируемого каталога. Секции:
[defaults]— значения по умолчанию для всех профилей.[[sync]]— профили зеркалирования (ПК → сервер, с зеркалированием удалений).[[backup]]— профили offload (выгрузка с последующим локальным удалением/архивом).
| Поле | Тип | Дефолт | Описание |
|---|---|---|---|
name |
строка | — (обязательно) | Уникальное имя профиля. |
local_root |
строка | — (обязательно) | Путь относительно конфига или абсолютный; должен существовать. |
remote_root |
строка | — (обязательно) | user@host:/path, alias:/path из ~/.ssh/config или локальный путь. |
exclude |
список | [] |
паттерны исключения (фильтры rsync, gitignore-подобные). |
include |
список | [] |
паттерны возврата (override exclude). |
delete |
bool | true (sync) / false (backup) |
Зеркалить удаления на сервере (--delete). |
dry_run |
bool | false |
Прогон без изменений. |
delete_threshold |
число | 100 |
Порог delete-guard по количеству удаляемых объектов. |
delete_threshold_pct |
число | 25 |
Порог delete-guard по доле (%) от объектов на сервере. |
force_delete |
bool | false |
Снять delete-guard для профиля. |
checksum |
bool | false |
Сверять файлы по контрольной сумме (--checksum). |
compress |
bool | false |
Сжатие при передаче (-z). |
partial_progress |
bool | false |
--partial --progress для больших файлов. |
bwlimit |
строка | — | Ограничение полосы (--bwlimit). |
ssh_opts |
список | [] |
Доп. опции ssh (применяются только к SSH-цели). |
after_push |
enum | nothing |
Только [[backup]]: delete / archive / nothing. |
verify |
bool | true |
Только [[backup]]: подтверждать передачу перед after_push. |
archive_dir |
строка | <local_root>/../_fs-archive/<profile>/<YYYY-MM-DD>/ |
Только [[backup]] при after_push = "archive". |
Поле профиля перекрывает одноимённое из [defaults]. Нарушение формата или валидации
(неуникальное name, несуществующий local_root, пустой/корневой remote_root,
неверный after_push, неверный тип) → код возврата 1 с указанием профиля и поля.
[defaults]
delete_threshold = 100
delete_threshold_pct = 25
[[sync]]
name = "site"
local_root = "."
remote_root = "myserver:/storage/site" # alias из ~/.ssh/config
delete = true
exclude = ["*.tmp", "*.log", "build/"]
include = ["keep.log"]
[[backup]]
name = "vault"
local_root = "./big-data"
remote_root = "myserver:/storage/vault"
after_push = "archive"
verify = trueСопоставление путей выполняет сам rsync через --filter-правила. Синтаксис —
фильтры rsync (gitignore-подобные): * (в пределах сегмента), ** (cross-segment),
?, [abc]/[a-z], завершающий / (только каталоги), ведущий/срединный / (якорь
к корню передачи = local_root). include возвращает исключённое (override) и
выигрывает над exclude.
Правила транслируются в --filter-аргументы rsync детерминированно. Поскольку
rsync действует по принципу «первое совпадение», порядок важен: сначала безусловные
артефакты, затем include (+), затем exclude (-).
Авто-исключение артефактов. Файлы .fs-sync.toml, .fs-log и .env исключаются
из передачи всегда и не возвращаются никаким include — иначе они уйдут на сервер,
а меняющийся на каждом прогоне .fs-log сломает идемпотентность.
Единый источник истины. Утилита нигде не сопоставляет пути собственным движком:
и передача, и определение области offload (что можно удалить локально) опираются на
тот же rsync с теми же фильтрами (rsync --list-only). Это исключает класс ошибок,
когда два разных матчера расходятся и offload удаляет непереданное.
Ограничение. Файл нельзя вернуть из каталога, исключённого целиком: rsync в
такой каталог не заходит. Чтобы вернуть вложенный файл, верните каталог явно
(include = ["dir/"]), затем задайте точечные правила внутри.
Профиль [[backup]] выгружает каталог на сервер (по умолчанию без серверного удаления
— накопительный архив) и затем, в зависимости от after_push, убирает локальные файлы:
nothing— оставить локально как есть;delete— удалить локальные файлы;archive— перенести вarchive_dir(по умолчанию рядом сlocal_root).
При verify = true (по умолчанию) перед локальным удалением/переносом выполняется
контрольная сверка (rsync --dry-run --checksum): удаляются/переносятся только
файлы, чья передача подтверждена. Частичный успех не трогает непереданное; сбой
передачи отменяет after_push целиком. В --dry-run локальные файлы не трогаются.
Перед фактическим зеркалированием удалений (delete = true) выполняется
предварительный --dry-run-расчёт. Если число удаляемых на сервере объектов превышает
delete_threshold (по количеству) или delete_threshold_pct (по доле от объектов
на сервере), удаление блокируется (код возврата 3) до явного подтверждения — флага
--force-delete или поля force_delete = true. Это защищает от случайной потери
данных на сервере при ошибочной настройке или перемещении локального каталога.
После боевого прогона в <каталог>/.fs-log дописывается блок (append, utf-8):
строка-метка времени YYYY-MM-DD HH:MM:SS, затем строки операций с отступом в 2
пробела, и пустая строка-разделитель между блоками. Маркеры операций:
+ <путь>— отправлено/обновлено;- <путь>— удалено на сервере;>> <путь>— выгружено и удалено/архивировано локально (offload).
Пустой список → (изменений нет). В журнал попадают только фактически выполненные
операции (план --dry-run не пишется). Формат общий для серии утилит над одним
каталогом. Сбой записи журнала не влияет на код возврата (только предупреждение в
stderr).
Уведомления (fire-and-forget) настраиваются в .env рядом со скриптом:
FSSYNC_WEBHOOK_URL=https://example.com/hook
FSSYNC_WEBHOOK_TOK=секретный-токенОбразец — .env.example. Без FSSYNC_WEBHOOK_URL уведомления отключены; токен
необязателен (при наличии добавляется заголовок Authorization: Bearer). Запрос —
POST {"text": ...} с минимальным таймаутом; ответ не проверяется, ошибки гасятся и
не влияют на прогон. По умолчанию уведомление отправляется при ошибках (код 2) и
срабатывании delete-guard (код 3).
Рекомендуется alias в ~/.ssh/config:
Host myserver
HostName server.example.com
User youruser
IdentityFile ~/.ssh/id_ed25519
Тогда remote_root = "myserver:/storage/site". Для cron:
- SSH-ключи без пароля;
- ключ хоста заранее добавлен в
~/.ssh/known_hosts(иначе первый запуск зависнет на подтверждении отпечатка). По умолчаниюStrictHostKeyChecking=noне подставляется — при необходимости задайте черезssh_opts.
Пример строки cron (ежедневно в 3:30):
30 3 * * * /path/to/fs-syncher/sync.sh /path/to/data >> /path/to/sync.out 2>&1Нативного rsync под Windows нет. Используйте WSL (рекомендуется) или cwrsync.
Учитывайте конвертацию путей (в WSL — /mnt/c/...) и нюансы прав/EOL. Пути для rsync
утилита приводит к posix.
Импортируйте из пакета, а не из подмодулей:
from syncher import (
load_config, parse_config, Config, Profile, ConfigError,
build_filters, filter_args,
build_command, run_rsync, delete_preflight, source_files, RsyncOutcome, DeletePlan,
run_offload, OffloadResult,
format_report, ProfileReport,
write_fs_log, send_webhook, main,
)В examples/ — запускаемая песочница без сети (см. examples/README.md):
python sync_fs.py examples --dry-runИтог --dry-run зафиксирован в examples/README.md и проверяется тестом.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pytest # тесты
ruff check syncher tests # линтер (импорты — isort)
mypy --strict syncher sync_fs.py # типыВсё должно проходить без ошибок. Код, тесты, примеры и документация меняются вместе.
Интеграционные тесты с реальным rsync пропускаются, если бинарь не установлен.