Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.

Repository files navigation

⚠️ ВНИМАНИЕ ⚠️ Репозиторий архивирован. Утилита доступна в составе пакета: fs-tools.

fs-syncher

Консольная утилита односторонней синхронизации локального каталога с сервером (ПК → сервер) через внешний 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 — ошибка ещё до запуска профилей).

Формат .fs-sync.toml

Файл лежит в корне синхронизируемого каталога. Секции:

  • [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

Правила include/exclude

Сопоставление путей выполняет сам 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/"]), затем задайте точечные правила внутри.

Offload (профиль [[backup]])

Профиль [[backup]] выгружает каталог на сервер (по умолчанию без серверного удаления — накопительный архив) и затем, в зависимости от after_push, убирает локальные файлы:

  • nothing — оставить локально как есть;
  • delete — удалить локальные файлы;
  • archive — перенести в archive_dir (по умолчанию рядом с local_root).

При verify = true (по умолчанию) перед локальным удалением/переносом выполняется контрольная сверка (rsync --dry-run --checksum): удаляются/переносятся только файлы, чья передача подтверждена. Частичный успех не трогает непереданное; сбой передачи отменяет after_push целиком. В --dry-run локальные файлы не трогаются.

Защита от массового удаления (delete-guard)

Перед фактическим зеркалированием удалений (delete = true) выполняется предварительный --dry-run-расчёт. Если число удаляемых на сервере объектов превышает delete_threshold (по количеству) или delete_threshold_pct (по доле от объектов на сервере), удаление блокируется (код возврата 3) до явного подтверждения — флага --force-delete или поля force_delete = true. Это защищает от случайной потери данных на сервере при ошибочной настройке или перемещении локального каталога.

Журнал .fs-log

После боевого прогона в <каталог>/.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).

Настройка SSH и запуск по таймеру

Рекомендуется 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

Ограничение Windows

Нативного rsync под Windows нет. Используйте WSL (рекомендуется) или cwrsync. Учитывайте конвертацию путей (в WSL — /mnt/c/...) и нюансы прав/EOL. Пути для rsync утилита приводит к posix.

Публичное API

Импортируйте из пакета, а не из подмодулей:

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 пропускаются, если бинарь не установлен.

About

Консольная утилита односторонней синхронизации локального каталога с сервером (ПК → сервер) через внешний rsync поверх SSH.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages