Skip to content

Repository files navigation

fs-tools

Обзор

Проект — набор кросс-платформенных CLI-утилит для обслуживания файловой системы, собранный в один устанавливаемый пакет с общим ядром (shared/): выбор каталога, доступ к единому .env, журнал .fs-log.log и веб-хук уведомления реализованы один раз и переиспользуются всеми режимами, а не дублируются в каждом.

Репозиторий можно использовать как:

  • Готовый инструментарий — четыре независимых режима CLI, каждый со своим extra (pip install -e ".[normalizer]" и т.д.), которые можно ставить и запускать по отдельности или все сразу через единый диспетчер fs-tools;
  • Референс архитектуры — как построить симметричный набор read-only проверок (checker/schemer) и мутирующих режимов (normalizer/syncher) на общем ядре, с единым контрактом терминального вывода, кодов возврата и журналирования;
  • Отправную точку — скопировать структуру (src/fs_tools/shared/, режимный пакет, tests/, examples/) под собственный набор файловых утилит и адаптировать/отключить готовые режимы.

Режимы:

Режим Команда Что делает
Нормализация имён fs-normalizer Рекурсивно приводит имена файлов и папок к единому виду: транслитерация в ASCII, даты в ISO, единый стиль разделителей и регистра. Идемпотентна: повторный прогон над уже нормализованным деревом ничего не меняет (кроме служебного журнала)
Проверка структуры fs-checker Рекурсивно сверяет дерево с правилами .fs-checker (синтаксис как у .gitignore, но смысл инвертирован: перечисленное обязано существовать) и печатает список отсутствующих путей. Структуру не меняет
Синхронизация fs-syncher Односторонняя синхронизация каталога с сервером (ПК → сервер) через внешний rsync по декларативному .fs-syncher.toml. Состав передачи, зеркалирование удалений (с защитой delete-guard) и выгрузка-offload задаются профилями
Проверка схемы fs-schemer Рекурсивно сверяет базу знаний с декларативными группами .fs-schemer.toml: обязательные/опциональные служебные файлы, точный текст заданной строки, запрет пустых групповых папок и файлов вне них. Структуру не меняет

Все режимы доступны и по отдельности, и через единый диспетчер fs-tools.

Требования

  • Python 3.11+.
  • Базовая зависимость: pathspec (gitignore-семантика для .fs-normalizer и негативов .fs-checker).
  • Опциональные зависимости по режимам (extras):
  • Внешние бинарники для синхронизации: rsync (обязателен), ssh (для SSH-целей).

Тяжёлые зависимости подгружаются лениво: можно поставить только нужный extra, и другие режимы не будут требовать чужих пакетов. Для выбора каталога GUI-пакеты не нужны: Windows/WSL — нативный IFileOpenDialog через powershell.exe, macOS — osascript, обычный Linux — ввод пути в терминале.

Установка

Проект рассчитан на локальную editable-установку (не на публикацию в PyPI):

python3 -m venv .venv
source .venv/bin/activate                                     # Windows: .venv\Scripts\activate
pip install -e ".[normalizer,checker,syncher,schemer]"        # все режимы
# или частично:
pip install -e ".[normalizer]"                                # только нормализатор
pip install -e ".[checker]"                                   # только проверка
pip install -e ".[syncher]"                                   # только синхронизация
pip install -e ".[schemer]"                                   # только проверка схемы
pip install -e ".[normalizer,checker,syncher,schemer,dev]"    # + инструменты разработки

После установки доступны команды fs-normalizer, fs-checker, fs-syncher, fs-schemer, fs-toolspython -m fs_tools).

Windows (Chocolatey + rsync)

Для запуска fs-syncher напрямую из Windows установите rsync через Chocolatey:

  1. Установите Chocolatey по официальной инструкции: https://chocolatey.org/install#individual
  2. Откройте терминал от имени администратора и установите rsync:
choco install rsync -y
  1. Проверьте доступность бинаря:
rsync --version

Если rsync не найден в IDE-терминале после установки, полностью перезапустите IDE и создайте новый терминал (чтобы обновился PATH).

Обёртки bin/

Для запуска без ручной подготовки окружения есть обёртки в bin/: при первом запуске они создают .venv в корне проекта, ставят пакет со всеми extra и экспортируют FS_TOOLS_HOME (нужно для поиска единого .env).

bin/normalize.sh [каталог]        # Linux/macOS (терминал) → fs-normalizer
bin/check.sh [каталог]            # Linux/macOS (терминал) → fs-checker
bin/sync.sh [каталог] [флаги]     # Linux/macOS (терминал) → fs-syncher
bin/scheme.sh [каталог]           # Linux/macOS (терминал) → fs-schemer
bin/normalize.command             # macOS (двойной клик в Finder)
bin/check.command                 # macOS (двойной клик в Finder)
bin/sync.command                  # macOS (двойной клик в Finder)
bin/scheme.command                # macOS (двойной клик в Finder)
bin/normalize.bat [каталог]       # Windows → fs-normalizer
bin/check.bat [каталог]           # Windows → fs-checker
bin/sync.bat [каталог] [флаги]    # Windows (через WSL/cwrsync) → fs-syncher
bin/scheme.bat [каталог]          # Windows → fs-schemer

Использование

Без аргумента каталог выбирается интерактивно (по умолчанию предлагается рабочий каталог). Каталог можно передать аргументом — тогда диалог не открывается:

fs-normalizer                           # нормализация: выбрать каталог в диалоге
fs-normalizer /path/to/dir              # без диалога
fs-normalizer /path/to/dir --dry-run

fs-checker                 # проверка: выбрать каталог в диалоге
fs-checker /path/to/dir    # без диалога

fs-syncher                 # синхронизация: выбрать каталог в диалоге
fs-syncher /path/to/dir    # без диалога

fs-schemer                 # проверка схемы: выбрать каталог в диалоге
fs-schemer /path/to/dir    # без диалога

fs-tools normalize /path/to/dir              # то же через диспетчер
fs-tools normalize /path/to/dir --dry-run
fs-tools check /path/to/dir
fs-tools sync /path/to/dir
fs-tools scheme /path/to/dir

python -m fs_tools normalize    # эквивалент fs-tools normalize

Флаг -c/--config PATH (во всех режимах) указывает файл конфигурации с произвольным именем — так в одной папке можно держать несколько конфигов под разными именами. Корнем запуска становится каталог этого файла, поэтому позиционный каталог не нужен (а если задан вместе с --config — игнорируется):

fs-schemer --config /home/kb/prod.fs-schemer.toml      # корень = /home/kb, конфиг = prod.fs-schemer.toml
fs-checker -c /home/kb/ci.fs-checker                   # корень = /home/kb, правила = ci.fs-checker
fs-syncher -c /home/kb/nightly.fs-syncher.toml         # корень = /home/kb, профили = nightly.fs-syncher.toml
fs-tools scheme -c /home/kb/staging.fs-schemer.toml    # то же через диспетчер

Активный файл конфига (даже с видимым именем) не обрабатывается самим режимом: нормализатор его не переименовывает, синхронизация не отправляет на сервер, проверка схемы не считает его «файлом вне групповой папки» — так же, как исключается скрытый конфиг по имени по умолчанию.

Запуск по таймеру — с фиксированным путём в команде, поэтому стартовый рабочий каталог не важен:

# crontab -e
0 3 * * * /path/to/fs-tools/bin/normalize.sh /mnt/disk/Home    # нормализация ночью
0 9 * * * /path/to/fs-tools/bin/check.sh /mnt/disk/Home        # проверка утром
0 1 * * * /path/to/fs-tools/bin/sync.sh /mnt/disk/Home         # синхронизация ночью
0 7 * * * /path/to/fs-tools/bin/scheme.sh /mnt/disk/Home       # проверка схемы утром

Режим нормализации (fs-normalizer)

Правила

Применяются к имени (для файлов — без расширения) строго по порядку:

# Правило Что делает
1 TransliterationRule Не-ASCII → ASCII (ь/ъ удаляются, апострофа не дают); разделители пути и управляющие символы из транслитерации (½1/2, \) → -; запрещённые на Windows < > : " | ? * (из «»<</>>) вырезаются
2 BracketsRule Скобки с числом/датой → убираются ((1)/[1]1); с текстом → сохраняются; непарные/несовпадающие → вырезаются
3 DateRule Даты → ISO YYYY-MM-DD; недостающие части → 00
4 SpaceToDashRule Пробелы → дефис; цепочки вокруг пробела схлопываются, намеренные file--improved сохраняются
5 TrimEdgeRule Обрезка «мусора» по краям (ведущий _ сохраняется; парная скобка на краю сохраняется; +/# — символы имени: C#, C++, notepad++)
6 LeadingZeroRule Одиночный числовой токен → с ведущим нулём (1_file01_file)
7 CaseRule Папки — с заглавной, файлы — в нижнем регистре (README сохраняется); у папок ведущий _ сохраняется (_private_Private)

Порядок важен: LeadingZeroRule после TrimEdgeRule, CaseRule — последним; это обеспечивает корректность и идемпотентность за один проход. Расширение файла не меняется. Скрытые объекты (на .) и корневой каталог не трогаются.

Примеры: Отчёт.TXTotchiot.TXT, Файл (1).docxfail-01.docx, 20.05.2020_dump2020-05-20_dump, отчёт за мартOtchiot-za-mart (папка).

Фильтр путей .fs-normalizer

Положите файл .fs-normalizer в нормализуемый каталог (как .gitignore в корне репозитория). Полный синтаксис gitignore поверх pathspec: *, **, ?, [abc], завершающий /, якоря (/foo — от корня, foo — basename на любой глубине), ! — возврат (override), порядок строк значим. Матчинг регистронезависим. Нет файла → фильтр выключен. Без правил-! внутрь исключённых каталогов обход не заходит; исключённые объекты в счётчики не попадают. Файл ЕСТЬ, но не удалось прочитать (нет прав, гонка удаления) — это не «нет файла»: прогон останавливается кодом 1 (стало симметрично checker/schemer), а не тихо переименовывает исключённое.

Коды возврата

Код Условие
0 прогон без реальных ошибок (безопасно пропущенные конфликты входят сюда)
1 ошибка запуска: каталог не выбран / не найден / не каталог, не установлен extra normalizer (Unidecode), либо .fs-normalizer есть, но не удалось прочитать
2 часть переименований не удалась (OSError: напр. зарезервированные имена Windows, длина пути)

Конфликт (занятое целевое имя) — безопасный пропуск, на код возврата не влияет.

Dry-run (--dry-run)

--dry-run строит план нормализации без переименования объектов. В отчёте режим помечается как dry-run. В этом режиме .fs-log.log содержит планируемые изменения.

Публичное API

from pathlib import Path

from fs_tools.normalizer import build_normalizer, FsNormalizer, load_fs_ignore, write_fs_log

build_normalizer().normalize("Отчёт 2020", is_dir=True)    # 'Otchiot_2020-00-00'

target = Path("/path/to/dir")
normalizer = FsNormalizer(build_normalizer(), load_fs_ignore(target))
renamed, skipped = normalizer.apply(target)
write_fs_log(target, normalizer.renames)                         # дописать .fs-log.log

renamed, skipped = normalizer.apply(target, dry_run=True)        # только план, без rename
normalizer.planned                                               # пары src -> dst для dry-run

Режим проверки (fs-checker)

Формат .fs-checker

Файл лежит в корне проверки и читается оттуда. Синтаксис близок к .gitignore, но перечисленное обязано существовать.

  • Кодировка utf-8-sig; разделитель сегментов — POSIX /. Комментарий — только ведущий #; пустые строки игнорируются; конечные пробелы обрезаются (значимый — \ ).
  • Сегменты: литерал, * (один уровень), ** (ноль и более), глоб внутри сегмента.
  • Мандат — последний сегмент. Завершающий / → строго каталог (is_dir()); без него — exists() (файл или папка).
  • Негативы ! работают через единый ordered pathspec-канал: матч по относительным путям якоря и мандата (anchor/require), поддержка масок **, *, ?, [], порядок строк значим (last-match-wins).
  • В checker ! — только исключение из проверки: re-include не используется. Ведущие ! схлопываются, поэтому !!/Code/PHP/** трактуется как !/Code/PHP/**.
# фиксированные каталоги — отдельными правилами-литералами
/Activities
/Activities/Web/Projects
# подстановка: в каждом занятии
/Activities/*/Projects
# строго каталог (завершающий /)
/Activities/Web/Projects/Addl/
# обязательный файл (мандат-файл)
/Activities/Web/Projects/Work/*/*/Data/project.md
# архивные проекты на любом уровне
/Activities/Web/Projects/**/_Archive/*/Back
# short pathspec-паттерн: исключить ветки _Archive на любой глубине
!_Archive

Фиксированные промежуточные каталоги описывают отдельными правилами-литералами: иначе их отсутствие маскирует более глубокие нарушения (нет якоря — нечего проверять).

Вывод и коды возврата

Каталог: /mnt/disk/Home
Отсутствуют пути (4):
  Activities/3D/Resources
  Activities/Web/Projects/Addl/safegrid.example/Data
  Activities/Web/Projects/Self/personal.example/Back
  Activities/Web/Projects/Work/Fabrikam/widgets.example/Data/project.md
Статус: warn. Найдены отсутствующие пути.
Сводка: проверено правил: 17; найдено каталогов-кандидатов: 22; отсутствует: 4; ошибок чтения: 0.

Каталог с ** в префиксе, который не удалось просканировать (нет прав, гонка удаления), — это не «нет якорей» (штатный случай), а техническая ошибка: отдельным блоком Ошибки чтения (N): с пометкой (ОШИБКА), статус повышается до error., а код возврата — до 2, даже если Отсутствуют пути пуст (иначе непросканированное поддерево тихо выпало бы из проверки).

Код Условие
0 нарушений нет
1 ошибка запуска: каталог не выбран / не найден / не каталог, нет .fs-checker
2 проверка выполнена: найдены отсутствующие пути и/или ошибки сканирования

Уведомления (веб-хук) и .env

Проверка всегда дописывает журнал .fs-log.log; при нарушениях дополнительно шлёт fire-and-forget веб-хук (POST {"text": ...} с заголовком Authorization: Bearer <токен>, если токен задан; сетевые ошибки гасятся и на код возврата не влияют).

Конфигурация — в едином .env проекта. Путь: FS_TOOLS_HOME/.env (переменную экспортируют обёртки bin/*), при отсутствии переменной — .env в текущем рабочем каталоге. Шаблон — .env.example:

FSCHECKER_WEBHOOK_URL=https://example.com/hook
FSCHECKER_WEBHOOK_TOKEN=секретный-токен

Особенности конфигурации веб-хука:

  • приоритет: переменные окружения процесса важнее значений из .env;
  • только HTTPS: не-https:// URL отвергается (токен не уходит по нешифрованному каналу).

Без FSCHECKER_WEBHOOK_URL уведомления отключены; токен необязателен.

Публичное API

from pathlib import Path

from fs_tools.checker import FsChecker, format_report, load_fs_rule

root = Path("/path/to/dir")
checker = FsChecker(load_fs_rule(root)).check(root)
print(format_report(root, checker))              # checker.missing — отсортированный список

Режим проверки схемы (fs-schemer)

Рекурсивно сверяет базу знаний с декларативными группами .fs-schemer.toml: дерево состоит из тематических узлов произвольной глубины, любой из которых может содержать групповые папки (_Knowledges, _Commands, …) с обязательными/опциональными служебными файлами и «обычными» файлами группы. Конфиг .fs-schemer.toml (без ведущей точки) лежит в переданном утилите каталоге и читается оттуда; по умолчанию этот же каталог и проверяется. Утилита не меняет структуру (read-only).

Если конфиг удобнее держать отдельно от проверяемого дерева (например, все конфиги утилит — в E:/Home, а база знаний — в E:/Home/Workspace/Warehouse), поле [defaults].apply_root задаёт каталог, который реально обходится — см. ниже.

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

[defaults]
apply_root = "Workspace/Warehouse"  # опционально: каталог проверки (см. ниже)
exclude_prefix = "_"                # префикс "служебных" файлов группы

[[group]]
name = "_Knowledges"
default_rule = { line = 1, text = "# Заметки", extensions = [".md"] }  # только .md

  [[group.file]]
  name = "_main.md"             # обязателен (optional не задан)
  line = 1
  text = "# Заметки"

  [[group.file]]
  name = "rules.md"
  optional = true                # опционален; если есть — контент проверяется
  line = 3
  text = "## Правила"

[[group]]
name = "Learn"                   # имя папки совпадает с ожидаемым заголовком
default_rule = { line = 1, mode = "folder", type = "contains" }  # обычные файлы: имя папки — подстрока строки 1

  [[group.file]]
  name = "_main.md"
  line = 1
  mode = "folder"
  prefix = "# "                  # строка 1 == "# " + имя папки (заголовок H1 от имени)

[[group]]
name = "_Resources"             # группа без собственных файловых правил — валидно

[[group]]
name = "_Commands"
strict = true                    # опция: прежнее строгое поведение для этой группы
  • [defaults].apply_root — опционально: путь каталога, который реально обходится и проверяется, если он отличается от каталога с .fs-schemer.toml (абсолютный либо относительный — от каталога конфига). Не задан → каталог проверки = каталог конфига (текущее поведение). .fs-log.log при этом остаётся рядом с конфигом (не переезжает в apply_root) — чтобы журналы всех режимов можно было держать в одном общем каталоге вместе с конфигами. Несуществующий apply_root — код возврата 1.
  • [defaults].check_loose_files — опционально (bool, дефолт true): включена ли проверка «файл вне групповой папки» (loose_file). false отключает её во всём дереве — файлы вне групп игнорируются, проверяются только объявленные группы; удобно, когда групповые папки (_Inf и т.п.) — редкие островки в большом дереве обычных файлов.
  • [defaults].exclude_prefix — префикс служебных файлов группы (дефолт _).
  • [[group]].name — basename групповой папки, матчится на любой глубине, регистрозависимо.
  • [[group]].default_rule — контент-правило {line, …} для всех видимых файлов группы, не начинающихся с exclude_prefixвключая файлы с записью group.file (правила накладываются, а не взаимоисключаются). Опционально extensions (whitelist, напр. [".md"]) и/или exclude_extensions (blacklist) — сужают круг читаемых файлов по расширению (регистронезависимо); можно задать любое из полей по отдельности, оба сразу (итог — «whitelist минус blacklist») или ни одного (читаются все обычные файлы, как раньше). Не подошедшие под фильтр файлы не читаются и не входят в «проверено файлов» — для бинарных файлов (.docx, .pdf, …) без фильтра это давало ложный read_error.
  • Ожидание строки — плоские параметры prefix + база + suffix (одинаково в default_rule и в [[group.file]]; prefix/suffix по умолчанию пустые):
    • mode = "text" (дефолт) — база берётся из литерала text (обязателен и непуст при этом режиме); напр. mode = "text", type = "contains", text = "Заметки", prefix = "# " требует вхождения подстроки «# Заметки» в строку line;
    • mode = "folder" — база вычисляется от имени папки-предка файла (поле text при этом недопустимо); уровень заголовка задаётся prefix ("# " — H1, "## " — H2);
    • level (целое ≥ 0, дефолт 0, только при mode = "folder") — на сколько уровней вверх от папки файла брать имя базы: 0 — сама папка (для групп — групповая), 1 — её родитель, 2 — прародитель и т.д. Кейс <домен>/_Inf/_main.md, где заголовок = имя домена, а не группы _Inf: mode = "folder", level = 1, prefix = "# "# <домен>. level выше корня проверки → ошибка конфига (код 1);
    • type = "equals" (дефолт) — строка line целиком равна ожиданию (точный заголовок вида «# <Имя>»); type = "contains" — ожидание лишь входит в строку line как подстрока.
  • [[group.file]] — механизм на конкретное имя файла: optional=false (дефолт) — файл обязателен; optional=true — отсутствие не нарушение, но при наличии контент-проверка обязательна. line обязателен в каждой записи (при mode = "text" — плюс text). name не обязан быть уникален в группе — несколько записей с одним name выполняются все независимо (несколько content-проверок одного файла); обязательность файла при дублях — «строже побеждает».
  • [[group]].strict (bool, дефолт false) — по умолчанию обход не спускается в подпапки группы: group.file/default_rule по-прежнему проверяются для файлов прямо в папке группы, но её вложенная структура (сторонние библиотеки, сэмплы, произвольная организация) целиком исключена из проверки — loose_file (F15) в ней не срабатывает. strict = true включает прежнее строгое поведение: подпапки группы заново классифицируются наравне с остальным деревом, и F15 в них работает как обычно (полезно, если внутри конкретной группы вложенность — ошибка, а не норма).

Категории нарушений

Тип Условие
missing_group_file обязательный [[group.file]] отсутствует в группе
bad_header строка line не совпала с ожиданием (mode = text/folder, type)
missing_line файл короче номера строки line
read_error файл не удалось прочитать (нет прав, не-UTF-8 содержимое и т.п.) — (ОШИБКА)
empty_group групповая папка не содержит ни одного видимого файла (в т.ч. вложенных)
loose_file файл лежит напрямую в тематическом узле, минуя групповые папки

.fs-schemer.toml в корне проверки под loose_file не попадает — он скрытый (ведущая точка), как и .fs-syncher.toml/.fs-checker.

Вывод и коды возврата

Каталог: /mnt/disk/Warehouse
Статус: error. Найдены нарушения структуры/контента.
Сводка: проверено групп: 6; проверено файлов: 9; нарушений: 5.

В терминал попадает только это (минимум информации); полный список нарушений (тип + путь, для контентных — ожидание/факт) дописывается в .fs-log.log, см. раздел «Журнал .fs-log.log».

Код Условие
0 нарушений нет
1 ошибка запуска: каталог не выбран / не найден / не каталог, нет/невалиден .fs-schemer.toml (в т.ч. несуществующий apply_root)
2 проверка выполнена, найдены нарушения

Уведомления (веб-хук) и .env

Проверка всегда дописывает журнал .fs-log.log; при нарушениях дополнительно шлёт fire-and-forget веб-хук (симметрично fs-checker). Конфигурация — в едином .env проекта, шаблон — .env.example:

FSSCHEMER_WEBHOOK_URL=https://example.com/hook
FSSCHEMER_WEBHOOK_TOKEN=секретный-токен

Без FSSCHEMER_WEBHOOK_URL уведомления отключены; токен необязателен; только https://.

Публичное API

from pathlib import Path

from fs_tools.schemer import FsSchemer, format_report, load_scheme_config

root = Path("/path/to/dir")
schemer = FsSchemer(load_scheme_config(root)).check(root)
print(format_report(root, schemer))              # schemer.violations — отсортированный список

Режим синхронизации (fs-syncher)

Односторонняя синхронизация локального каталога с сервером (ПК → сервер) через внешний rsync поверх SSH (или в локальный каталог). Утилита — тонкая обёртка над rsync: читает и валидирует .fs-syncher.toml, транслирует правила include/exclude в фильтры rsync (сопоставление путей выполняет сам rsync), запускает команду, разбирает итог, дописывает .fs-log.log и при необходимости шлёт веб-хук.

Без аргумента каталог выбирается интерактивно (диалог проводника на Windows и в WSL, диалог macOS, либо ввод пути в терминале на Linux). Каталог можно передать аргументом — тогда диалог не открывается.

fs-syncher                                # выбрать каталог и запустить все профили
fs-syncher /path/to/dir                   # все профили из .fs-syncher.toml
fs-syncher /path/to/dir --dry-run         # план без передачи/удаления
fs-syncher /path/to/dir --profile site    # только профиль «site» (флаг повторяемый)
fs-tools sync /path/to/dir                # то же через диспетчер

Флаги: --profile NAME (повторяемый), --all (все профили — поведение по умолчанию), --dry-run (приоритетнее dry_run профиля), --force-delete (снять delete-guard).

Windows: нативного rsync нет — запускайте режим через WSL (рекомендуется) или cwrsync. Пути для rsync приводятся к posix.

Простой Windows-сценарий (cwrsync, без WSL)

Конфиг держим в корне E:\Home и задаём относительный источник только для нужной папки:

[[sync]]
name = "access"
local_root = "Access"
remote_root = "user@host:/mnt/disk/Home/Access"
delete = false
checksum = true

Запуск:

bin\sync.bat "E:\Home" --dry-run --profile access

Такой профиль синхронизирует только E:\Home\Access и не требует UNC-путей вроде \\localhost\....

Сценарий через WSL (рекомендуется)

Если данные лежат на Windows-диске, запускайте sync.sh из WSL, а каталог передавайте в формате /mnt/<disk>/...:

sudo apt update
sudo apt install -y rsync openssh-client
/home/<user>/Home/Components/fs-tools/bin/sync.sh /mnt/e/Home --dry-run --profile access

Здесь /mnt/e/Home соответствует E:\Home, а local_root = "Access" в .fs-syncher.toml остаётся относительным и безопасно ограничивает область синхронизации.

Troubleshooting: права 777 на сервере (WSL/drvfs)

WSL монтирует Windows-диски через drvfs (/mnt/e/...): у NTFS нет настоящих Unix-прав, и stat часто показывает 777. fs-syncher запускает rsync с -a, который сохраняет права источника — на Linux-сервере появляются 777 у переданных или обновлённых объектов.

Проверка на WSL:

ls -la /mnt/e/Home/<папка>/<файл>
stat -c '%a %n' /mnt/e/Home/<папка>/<файл>

Рекомендуемая настройка в .fs-syncher.toml[defaults] или в профиле):

[defaults]
chmod = "D755,F644"

chmod добавляет rsync --chmod=D755,F644 (каталоги 755, файлы 644 на приёмнике). При дефолтном preserve_perms = true rsync исправляет и новые, и уже лежащие на сервере объекты с 777, даже если содержимое не менялось.

preserve_perms = false (rsync --no-perms) не комбинируйте с chmod для drvfs: тогда --chmod применяется только к переданным файлам, а неизменённые 777 на сервере останутся. Поле preserve_perms = false без chmod имеет смысл для POSIX→POSIX, когда нужны права по umask приёмника.

После перехода на один только chmod следующий прогон восстановит уже лежащие на сервере 777. Если в конфиге по-прежнему preserve_perms = false вместе с chmod, неизменённые файлы с 777 не починятся — смените конфиг или выполните find … -exec chmod … на приёмнике. checksum = true права не восстанавливает.

Альтернатива без правок конфига — включить metadata в WSL (/etc/wsl.conf, секция [automount], опция options = "metadata,umask=022"), затем wsl --shutdown.

Troubleshooting: The source and destination cannot both be remote

Эта ошибка появляется, когда локальный путь источника передан в rsync как E:/..., и он ошибочно трактуется как host:path. В актуальной версии fs-syncher локальный Windows-путь автоматически нормализуется в локальный формат /cygdrive/e/...; если ошибка повторяется, обновите пакет до текущей версии.

Troubleshooting: code 12 / Permission denied (publickey) на Windows

Для cwrsync-стека rsync и ssh должны работать в одном окружении. Если в системе одновременно есть Windows OpenSSH и chocolatey-ssh, задайте транспорт явно:

$env:HOME = "/cygdrive/c/Users/<user>"
$env:RSYNC_RSH = "/cygdrive/c/ProgramData/chocolatey/lib/rsync/tools/bin/ssh.exe"
bin\sync.bat "E:\Home" --dry-run --profile access

Если ключ защищён passphrase, используйте ssh-agent в выбранном окружении или выделенный ключ для автоматизированных запусков.

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

Конфиг лежит в корне синхронизируемого каталога. Разбор — стандартный tomllib.

  • [defaults] — значения по умолчанию для всех профилей;
  • [[sync]] — профили зеркалирования (ПК → сервер, с зеркалированием удалений);
  • [[backup]] — профили выгрузки-offload (передача + локальное удаление/архив).

Поля профиля:

Поле Назначение
name уникальное имя профиля (обязательно)
local_root путь относительно конфига или абсолютный (обязателен, должен существовать)
remote_root user@host:/path, alias:/path из ~/.ssh/config или локальный путь (обязателен; локальный отсчитывается от каталога конфига)
exclude / include списки gitignore-подобных паттернов
delete зеркалить удаления (дефолт: true для sync, false для backup)
dry_run дефолт false
delete_threshold / delete_threshold_pct пороги delete-guard (по количеству — дефолт 100; по доле % — дефолт 25)
force_delete снять delete-guard (дефолт false)
checksum, compress, partial_progress опции передачи (bool)
bwlimit ограничение полосы (строка)
ssh_opts доп. опции ssh (список строк; только для SSH-целей)
preserve_perms сохранять права источника (дефолт true; false → rsync --no-perms; с chmod — только к переданным файлам)
chmod явные права на приёмнике (строка, синтаксис rsync --chmod, напр. D755,F644; для WSL/drvfs — основной обход 777)
after_push только [[backup]]: delete / archive / nothing (дефолт nothing)
verify только [[backup]]: сверка перед offload (дефолт true)
archive_dir только [[backup]]: каталог архива (дефолт <local_root>/../_fs-backup/<profile>/<YYYY-MM-DD>/)

Поле профиля перекрывает одноимённое из [defaults]. Валидация (ошибка → код 1 с указанием профиля и поля): уникальность name; существование local_root; безопасный remote_root (не корень /, непустой путь после :); корректный enum after_push; типы полей.

Правила include/exclude и трансляция в rsync

Сопоставление путей выполняет сам rsync через --filter-правила — он единственный источник истины и для передачи, и для определения области offload (через rsync --list-only). Своего матчера в режиме нет.

Синтаксис правил (gitignore-подобный): * (в пределах сегмента), ** (cross-segment), ?, [abc]/[a-z], завершающий / (только каталоги), ведущий/срединный / (якорь к корню передачи). exclude исключает объект; include возвращает его (override).

rsync обрабатывает фильтры по принципу «первое совпадение», поэтому порядок:

1. безусловные артефакты:  - /.fs-syncher.toml, - /.fs-log.log, - .env
2. include (override):     + <pattern>
3. exclude:                - <pattern>

Артефакты .fs-syncher.toml, .fs-log.log, .env исключаются всегда и не возвращаются никаким include (иначе меняющийся .fs-log.log сломал бы идемпотентность). Файл нельзя вернуть из исключённого целиком каталога — rsync в него не заходит; возвращайте каталог явно (include = ["dir/"]), затем точечные правила внутри.

Offload ([[backup]]) и delete-guard

  • Offload-безопасность: при after_push ∈ {delete, archive} локальный файл убирается только после подтверждённой передачи (verify = true сверяет повторным rsync --dry-run --checksum). Сбой передачи отменяет after_push целиком; --dry-run локальные файлы не трогает; частичный успех не трогает непереданное.
  • Сохранение якорных include-каталогов: при [[backup]] опустевшие каталоги, явно заданные якорными include-правилами (например **/Back/), не удаляются после offload. Промежуточные/вложенные каталоги сохраняются только если заданы отдельным якорным include-паттерном (например **/Back/**/Fold/**/). Для одного и того же якоря сохраняется ближайший уровень: при .../Back/Back внутренний Back удаляется, если не задан отдельным более точным паттерном.

local_root/remote_root/archive_dir и after_push = "archive" — это внешний контракт fs-syncher. Внутри режим по-прежнему использует свою модель профиля и сборку команды rsync; семантика rsync и других библиотек не переопределяется.

  • Delete-guard: серверные удаления выше порога (delete_threshold по количеству или delete_threshold_pct по доле) блокируются (код 3) до явного подтверждения (--force-delete или force_delete = true).

Коды возврата

Код Условие
0 успех, включая «изменений нет»
1 ошибка запуска: нет каталога/.fs-syncher.toml, ошибка валидации, нет rsync (или ssh при SSH-цели)
2 rsync/offload завершились ошибкой (передача неполная)
3 остановлено delete-guard (превышен порог удаления без подтверждения)

Итог прогона — наихудший среди профилей по шкале 0 < 2 < 3 (код 1 — ошибка до запуска профилей). Сбой записи .fs-log.log на код возврата не влияет.

Уведомления (веб-хук) и .env

При наихудшем коде прогона 2 или 3 режим шлёт fire-and-forget веб-хук (POST {"text": ...} с заголовком Authorization: Bearer <токен>, если токен задан; сетевые ошибки гасятся и на код возврата не влияют). Конфигурация — в едином .env проекта (FS_TOOLS_HOME/.env, фолбэк — .env в текущем каталоге), шаблон — .env.example:

FSSYNCHER_WEBHOOK_URL=https://example.com/hook
FSSYNCHER_WEBHOOK_TOKEN=секретный-токен

Особенности: приоритет — переменные окружения процесса важнее значений из .env; только https:// URL (токен не уходит по нешифрованному каналу). Без FSSYNCHER_WEBHOOK_URL уведомления отключены; токен необязателен.

Публичное API

from pathlib import Path

from fs_tools.syncher import load_config, build_command, run_rsync

config = load_config(Path("/path/to/dir"))
profile = config.profiles[0]
outcome = run_rsync(build_command(profile, dry_run=True, delete=profile.delete))
print(outcome.sent, outcome.deleted)

Журнал .fs-log.log

Все режимы пишут единый журнал .fs-log.log в выбранный каталог (общий формат из fs_tools.shared.log): блок с меткой времени, строками Инструмент: normalizer|checker|syncher|schemer, Режим: production|dry-run, Результат: и строками тела:

  • нормализатор — последовательность событий в порядке выполнения: old -> new, (КОНФЛИКТ) old -> new, (ОШИБКА) old -> new: <текст> либо (изменений нет);
  • проверка — отсутствующие пути (ошибки сканирования **-обхода — впереди списка, с пометкой (ОШИБКА)) либо (нарушений нет);
  • синхронизация — операции с маркерами: + <путь> (отправлено/обновлено), - <путь> (удалено на сервере), >> <путь> (выгружено и удалено/архивировано локально), а также (КОНФЛИКТ)/(ОШИБКА) по профилям — тоже в хронологическом порядке; при пустом результате пишется (изменений нет);
  • проверка схемы — строки нарушений (тип + путь, для контентных — ожидание/факт; для read_error — пометка (ОШИБКА) и текст исключения) либо (нарушений нет).

Файл скрыт, создаётся при отсутствии и дополняется при повторных запусках (намеренное исключение из идемпотентности), добавлен в .gitignore. Журнал пишется и в production, и в dry-run; во втором случае фиксируется последовательность dry-run-событий без применения изменений.

Примеры

В examples/ — четыре песочницы: examples/normalizer/ (фикстуры по правилам + скрипты отката reset.*), examples/checker/ (дерево с .fs-checker и намеренно отсутствующими путями), examples/syncher/ (источник + локальные приёмники, прогон без сети, канонический --dry-run) и examples/schemer/ (.fs-schemer.toml вынесен из Warehouse/ через apply_root, намеренные нарушения F1–F15). Подробности — в README соответствующих секций.

Аудит по правилам

Для стабильного аудита используй project skill audit-governor с фиксированными режимами:

  • audit changed — аудит внесенных правок;
  • audit full — полный аудит проекта.

Рекомендуемые короткие запросы:

  • Запусти audit changed и доведи до полного green.
  • Запусти audit full и доведи до полного green.

Обязательный цикл проверок после каждой серии правок:

.venv/bin/python -m pytest -q
.venv/bin/python -m pylint --persistent=n --recursive=y src tools tests/*
.venv/bin/python -m ruff check .
.venv/bin/python -m mypy --strict -p fs_tools

Цикл повторяется до полного green по всем четырем командам.

Разработка

pytest                                    # тесты
pylint --recursive=y src tools tests/*    # Pylint (охват src, tools и tests/*)
ruff check .                              # линтер (в т.ч. порядок импортов, isort)
mypy --strict -p fs_tools                 # проверка типов
python -m build                           # сборка sdist + wheel

Раскладка тестов зеркалит пакет: tests/shared/, tests/normalizer/, tests/checker/, tests/syncher/, tests/schemer/ (режим --import-mode=importlib). Код проходит ruff и mypy --strict без замечаний. Интеграционные тесты режима синхронизации пропускаются, если в системе нет rsync.

Каталог tools/ — служебный инструментарий репозитория, в дистрибутив он не входит. В tools/hooks/ лежат форматтеры документации и кода (компактная форма markdown-таблиц, колонка inline-комментариев, автоправки ruff) вместе с тестами-барьерами, которые ловят правки в обход форматтеров; их запускает Claude Code после каждой правки файла, а pytest прогоняет как обычные тесты. Подробности — tools/hooks/README.md.

About

Кроссплатформенный пакет CLI-утилит для работы с файловой системой: fs-normalizer (нормализация имён), fs-checker (проверка структуры по .fs-check) и fs-syncher (односторонняя синхронизация через rsync) с единым ядром, общими обёртками запуска и поддержкой Windows/WSL/Linux/macOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages