Привет, Хабр!
yarsync — Yet Another Rsync — предназначен для синхронизации данных между несколькими устройствами, более точно — между файловыми системами в Unix-подобных средах. yarsync обладает интерфейсом, похожим на git, и является Python-обёрткой вокруг программы rsync. Программа доступна под свободной лицензией GPL v3.0 на github (я автор).
yarsync работает там, где есть Питон и rsync.
Данные могут синхронизироваться локально или между разными компьютерами (в таком случае на удалённой машине также должен быть установлен rsync). Кроме того, файловые системы должны поддерживать жёсткие ссылки (hard links). Популярные системы, поддерживающие жёсткие ссылки — ext2-ext4, HFS+, а также NTFS. Не поддерживают жёсткие ссылки FAT, exFAT (часто используемые на флеш-накопителях).
Говоря простыми словами, допустим, что у вас есть компьютеры дома и на даче. У вас есть папка с книгами и статьями по программированию, которые вы собирали долгие годы, и которой регулярно пользуетесь (её копиями на разных машинах). Вы хотите, чтобы эти копии были одинаковы — то есть в идеале чтобы можно было работать с данными на разных компьютерах (добавлять новые статьи, удалять ненужные, переименовывать и перемещать файлы и папки), а затем эти изменения легко переносились на другие копии. Это и делает yarsync, отслеживая изменения и позволяя эффективно синхронизировать данные через доступный сервер или внешний накопитель (жёсткий диск).
Прежде чем говорить о дизайне, стоит обозначить цели yarsync, а ими являются:
-
удобство пользователя. Оно не только включает в себя привычный интерфейс, но и снижает риск ошибок.
-
производительность. rsync использует эффективный алгоритм для передачи данных (передавая только различия). Уже переданные файлы не передаются вновь (даже если были перемещены или переименованы). Программа вызывается когда необходимо и не занимает постоянно процессор и память.
-
надёжность. Первый выпуск rsync был в 1996 году, и с тех пор она является практически стандартной программой для синхронизации, то есть проверена множеством пользователей (к сожалению, мне не удалось найти даже примерное их число), и поддерживается и развивается по сегодняшний день (последняя версия вышла пять дней назад).
-
прозрачность для системы. Служебная информация (коммиты) является обычными файлами, не требующими упаковки и распаковки.
Далее читателю предлагается оценить, насколько близко эти цели оказались достигнуты.
Начало работы
Скопируйте программу:
$ git clone https://github.com/ynikitenko/yarsync
Внутри репозитория находится подпапка yarsync, в ней питоновский модуль yarsync.py и исполняемый файл yarsync (вызывающий первый). Нужно добавить путь к этим файлам, например, в ~/.bashrc:
export PATH=$PATH:~/yarsync/yarsync export PYTHONPATH=$PYTHONPATH:~/yarsync
Если программа находится и работает, то для изучения простых команд создадим новую директорию
me@myhost$ mkdir ~/tmp me@myhost$ cd ~/tmp
Инициализируем репозиторий:
me@myhost$ yarsync init # init configuration for myhost mkdir .ys create configuration file .ys/config.ini
Как видно из выхода программы, при инициализации создаётся скрытая директория .ys с конфигурационным файлом config.ini (он пока пуст, и подробнее мы обсудим его ниже).
Все служебные данные будут находиться только внутри директории .ys (также при частом использовании я использую для yarsync псевдоним (alias) ys).
Для инициализации репозитория с существующими данными можно вызвать yarsync init внутри нужной директории. Если репозиторий уже был инициализирован, то эта команда остаётся безопасной (то есть ничего не делает).
Различные состояния репозитория фиксируются в коммитах, которые создаются с помощью команды commit:
me@myhost$ yarsync commit rsync -a --link-dest=../../.. --exclude=/.ys --exclude=/.ys/* /home/me/tmp/ /home/me/tmp/.ys/commits/1650462990_tmp mv /home/me/tmp/.ys/commits/1650462990_tmp /home/me/tmp/.ys/commits/1650462990 mkdir /home/me/tmp/.ys/logs commit 1650462990 created When: Wed, 20 Apr 2022 16:56:30 MSK Where: me@myhost
Как можно видеть, вывод программы в данный момент довольно подробный (часто публикуются полные команды rsync).
Сначала создаётся временный коммит в директории .ys/commits. С помощью команды rsync в директории коммита создаются жёсткие ссылки файлов из рабочей директории (то есть всех файлов кроме служебной директории). В данный момент, поскольку файлов нет, то коммит будет пустым.
Если всё прошло удачно, то коммит перемещается в директорию без суффикса _tmp. Кроме того, создаётся директория .ys/logs, куда записывается описание коммита (в нашем случае .ys/logs/1650462990.txt содержит время, пользователя и машину, где был создан коммит).
Название коммита — это число секунд с начала эпохи (Unix-время начинается 1 января 1970, 00:00:00 UTC), получаемое с помощью функции Python time.time. Это универсальное время, то есть названия коммитов будут упорядочены вне зависимости от часового пояса на различных машинах.
Также можно создать описание коммита с помощью опции -m:
$ yarsync commit -m 'second commit' ...
Давайте добавим в репозиторий новый файл:
$ touch example.txt $ yarsync status rsync -aun --delete -i --exclude=/.ys --exclude=/.ys/* --outbuf=L /home/me/tmp/ /home/me/tmp/.ys/commits/1650463725 Changed since head commit: .d..t...... ./ >f+++++++++ example.txt No syncronization information found.
Вывод программы даётся в формате опции rsync -i (—itemize-changes). Первая строка обозначает, что корневая директория (‘d’) не изменилась (‘.’), а точнее изменилась только её временная метка (‘t’). На следующей строке мы видим, что со времени последнего («головного») коммита (более по-русски будет сказать «снимка») появился наш новый файл. Как и ранее, директория .ys не принимается во внимание (—exclude), и никаких изменений при запросе статуса не происходит (-n, —dry-run).
Также в директории .ys можно создать файл rsync-filter с синтаксисом фильтров rsync (он очень богатый, смотрите его руководство). Пример его содержания (комментарии разрешены):
# data can be copied separately - /data
В данном случае мы исключаем папку tmp/data из репозитория и игнорируем или синхронизируем её отдельно. Выделение отдельных подрепозиториев удобно в организации работы, но на носителях резервных копий удобнее линейная структура, чтобы можно было не искать вложения при синхронизации (хотя это можно решить, когда мы будем обсуждать работу с несколькими репозиториями одновременно).
Существующие коммиты можно посмотреть командой
$ yarsync log
Полный список команд можно получить с помощью yarsync —help. Кроме того, директория .ys может находиться вне синхронизируемого каталога (с помощью опций —config-dir и —root-dir). Моя первая публикация на Хабре была о статических страницах сайта, которые поддерживали контроль версий с помощью чистого (bare) git-репозитория в отдельной папке.
Содержимое коммитов — это файлы и папки корневого репозитория (на момент их создания). Их можно просматривать с помощью обычного менеджера файлов или вызывать в них из терминала стандартные команды вроде find. Если вы удалили файл в рабочей директории, но потом решили его восстановить, то можете скопировать его (создать жёсткую ссылку) из коммита, где он был. Если же, напротив, вам не нужны старые файлы, то вы можете свободно удалить старые коммиты rm -rf .ys/commits/, при этом ни рабочая директория, ни инфраструктура yarsync не пострадают.
Синхронизация
Информация о репозиториях находится в файле .ys/config.ini. Создадим простую конфигурацию для копии наших данных в папке ~/tmp2:
[tmp2] # empty host means local host host = path = /home/me/tmp2
Если мы попробуем скопировать репозиторий туда с помощью yarsync push tmp2, то получим ошибку. Программа проверяет, что назначение (destination) действительно является корректным репозиторием (что не так, поскольку мы его ещё не создали). Также перед отправкой данных необходимо сохранить (commit) локальные изменения. Если мы уверены, что папка ~/tmp2 пуста или не существует, то мы можем клонировать туда наш репозиторий с помощью ключа -f, —force:
$ yarsync push -f tmp2 # rsync -avHP --delete-after --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp/ /home/me/tmp2/
При переходе в ту папку, мы увидим, что она идентична нашему первому репозиторию, как и коммиты и их история (проверьте с помощью yarsync log). Хотя коммиты и логи копируются полностью, конфигурационные файлы (config.ini, rsync-filter и другие в папке .ys) не копируются, то есть независимы друг от друга.
Ключ rsync -H означает связывание жёстких ссылок в назначении (в нашем случае tmp2) таким же образом, как и в источнике. Если мы посмотрим индексные дескрипторы (inodes) файлов ls -i example.txt в tmp и tmp2, то мы увидим, что они отличаются — при этом внутри одного клона они совпадают в коммитах и рабочей директории.
Ключ —delete-after требует, чтобы перед реальными изменениями rsync просканировал все файлы, то есть собрал все существующие жёсткие ссылки. Если между двумя репозиториями есть синхронизированный коммит, и в одном из них файл в последующем коммите был перемещён, то rsync увидит совпадающий дескриптор и не будет вновь пересылать существующий файл.
Если мы в процессе работы создали новый коммит в tmp2, то мы можем также перенести изменения обратно в tmp:
$ cd ~/tmp $ yarsync pull tmp2
Разумеется, в конфигурационном файле в нескольких секциях могут быть настройки для большего числа репозиториев (tmp3 и пр.). Полный синтаксис файла указан в модуле configparser стандартной библиотеки.
Часто бывает, что путь к репозиторию меняется. Например, если мы копируем данные по сети, то DHCP может выдать новый ip-адрес компьютера, а при подключении жёсткого диска на лету может быть сгенерирован уникальный путь в /run/media. В таком случае можно использовать в конфигурации переменную окружения:
[my_drive] path = $MYDRIVE/programming
и если мы зададим переменную MYDRIVE, то путь будет определён корректно:
$ export MYDRIVE=/run/media/my_drive $ yarsync push my_drive
Кроме того, при синхронизации с другим репозиторием об этом сохраняется информация в .ys/sync.txt. В нашем случае в этом файле будет
1650468609,tmp2
то есть номер коммита и название другого репозитория. Также информация о синхронизации будет отображаться в командах status и log:
$ yarsync log commit 1650468609 <-> tmp2 When: Wed, 20 Apr 2022 18:30:09 MSK ...
Слияние версий
Когда у нас есть несколько реплик данных и мы регулярно переносим изменения из одной в другую (либо если одной из них мы пользуемся только для доступа к файлам, а все изменения производим в другой), то мы можем довольно долго так работать без каких-либо сложностей. Однако в какой-то момент может возникнуть ситуация, что наши истории коммитов разошлись (мы добавили новые файлы и в одну, и в другую реплику), и нам необходимо установить, какое состояние рабочей директории должно считаться корректным для всех репозиториев. В этом случае мы должны провести слияние версий (merge).
Допустим, наши репозитории в tmp и tmp2 синхронизированы. Создадим новые коммиты в каждой из реплик:
$ cd ~/tmp2 $ touch B $ yarsync commit -m 'Add B' ... commit 1650480050 created
и аналогично добавим файл ‘A’ в tmp. Теперь, когда мы попытаемся отправить данные из tmp в tmp2, то программа зафиксирует различающиеся коммиты и выдаст ошибку:
$ yarsync push tmp2 Nothing to commit, working directory clean. # local repository is 1 commits ahead of tmp2 # rsync -avHP --delete-after --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp/ /home/me/tmp2/ rsync --list-only /home/me/tmp2/.ys/commits/ ! destination has commits missing on source: 1650480050, synchronize these commits first: 1) pull missing commits with 'pull --new', 2) push if these commits were successfully merged, or 2') optionally checkout, 3') manually update the working directory to the desired state, commit and push, 2'') --force local state to remote (removing all commits and logs missing on the destination).
Как мы видим, предлагается несколько вариантов действий. Самое простое, если мы уверены, что в tmp2 не актуальные данные — записать туда состояние репозитория tmp, удалив все новые файлы с помощью опции push -f. Команда push проверяет, что в источнике все изменения были сохранены в коммит. В общем случае это невозможно сделать в удалённом репозитории (rsync не может копировать данные между двумя удалёнными машинами), поэтому изменения на другой машине могут быть не сохранены в её локальный коммит, и поэтому у команды pull опция -f отсутствует. Мы требуем, чтобы синхронизировались только сохранённые состояния — за исключением, о котором ниже.
У команды pull есть опция —new (которой нет у push), которая переносит только новые файлы с удалённого репозитория (не уничтожая локальные файлы, которые там отсутствуют):
$ yarsync pull --new tmp2 ... rsync --list-only /home/me/tmp2/.ys/commits/ # rsync -avHP --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp2/ /home/me/tmp/ merge 1650480038 and 1650480050 manually and commit (most recent common commit is 1650468609)
Как мы видим, в команде rsync здесь отсутствует флаг delete. Программа изучает коммиты в удалённом источнике, находит последний общий коммит (если он есть) и указывает последние коммиты в локальном и удалённом репозиториях, которые нужно синхронизировать.
В данный момент нам могут помочь несколько других команд:
$ yarsync diff 1650480038 1650468609 ... >f+++++++++ A
показывает, что чтобы перейти от общего коммита 1650468609 к 38-му (надеюсь, сокращение понятно), нужно добавить файл А. Так же мы можем посмотреть, что изменилось на другом репозитории с общего коммита (поскольку последний удалённый коммит уже скопирован локально). В данном случае это тривиально, но если вы в последний раз синхронизировали другую машину год назад, то эта информация будет очень полезна. Кроме того, поскольку наши коммиты хранятся в файловой системе, мы можем просто сравнить их с помощью diff -r (хотя придётся писать к ним пути), поэтому эта команда скорее для удобства.
В данный момент в локальном репозитории находится объединение рабочих папок обоих реплик:
$ yarsync status ... Changed since head commit: >f+++++++++ A Merging 1650480038 and 1650480050 (most recent common commit 1650468609). # local repository is 2 commits ahead of tmp2 $ ls A B example.txt
новым файлом считается A, поскольку последний коммит (головной) был сделан в tmp2 (при этом в рабочей директории находятся оба файла).
Информация об объединении находится в файле .ys/MERGE.txt, который создаётся автоматически. Если мы сейчас сделаем commit, то этот файл будет удалён, а информация о слиянии добавится в log.
Но представим ситуацию, когда мы действительно пять минут назад создали новый небольшой коммит в tmp2, однако до этого очень долго работали в tmp, удаляли и переименовывали многие файлы в рабочей директории, и теперь вместе с состоянием tmp2 мы вернули все эти файлы обратно (вместе с уже переименованными). В таком случае мы можем восстановить более актуальный коммит с помощью команды checkout:
$ yarsync checkout 1650480038 rsync -au --delete -i --exclude=/.ys --exclude=/.ys/* --outbuf=L /home/me/tmp/.ys/commits/1650480038/ /home/me/tmp *deleting B .d..t...... ./
Когда мы выполняем checkout, то головным (head) коммитом становится не самый последний, а тот, который мы загрузили. В частности, если мы ничего не меняли, то команда status будет показывать разницу с загруженным коммитом (а не последним), с добавлением строки
Detached HEAD (see 'yarsync log' for more recent commits)
Информация о головном коммите (если он не самый последний) сохраняется в .ys/HEAD.txt.
Если мы решим, что файл B нам больше не нужен, то мы можем прямо сейчас сделать коммит и отправить итоговую версию в tmp2:
$ yarsync commit -m 'Merge.' $ yarsync push tmp2
Во время коммита и MERGE.txt, и HEAD.txt будут автоматически удалены, и новый коммит будет считаться корректным состоянием репозитория. Поскольку все коммиты с tmp2 уже были скопированы локально, то сложностей с push уже не возникнет.
Поскольку старые коммиты могут произвольно удаляться, то может возникнуть ситуация, что в другом репозитории может сохраниться старый коммит, но при этом дальнейшая история будет совпадать с локальной копией (и последний коммит там будет среди локальных). В таком случае можно будет либо удалить тот старый коммит, либо перенести все коммиты pull —new, и в этом случае локальный репозиторий автоматически загрузит корректный (самый последний локальный) коммит. Возможно, что с развитием программы появятся новые эвристики, но в общем случае слияние состояний может проводиться только вручную — по описанному выше алгоритму.
Реализация
Написание исполняемой программы на Питоне не похоже на создание обычного модуля, а работа с командами не похожа на создание объектов с состояниями.
Как я указал выше, есть отдельный питоновский модуль yarsync.py. Поскольку не хотелось бы, чтобы пользователь был вынужден набирать лишние три символа в конце команды, то потребовалось создавать отдельный исполняемый файл yarsync (вызывающий первый). При этом питоновский модуль также нужен: его очень удобно тестировать с помощью pytest.
В одной из ранних версий я пытался зафиксировать опции rsync в отдельном объекте, но в итоге у меня остался только класс YARsync. Большинство его методов приватные. Более того, в отличие от привычных объектов, большинство его методов могут быть недоступны: если мы вызываем yarsync status, то происходит инициализация с данными аргументами командной строки, и метод _pull_push (они объединены, поскольку отличаются для rsync только порядком последних аргументов) мы вызвать просто не сможем, потому что неизвестен удалённый репозиторий. Огромную работу с всевозможными аргументами командной строки делает стандартный модуль argparse, а rsync вызывается с помощью subprocess.
Безопасность
Обычно парсинг конфигурационных файлов (тем более с заменой переменных окружения) может быть небезопасным. Если директория .ys отсутствует в текущем каталоге, то она ищется в его родительских каталогах (как в git). Я вызываю yarsync в проверенных директориях, однако в configparser ничего не говорится о том, чтобы его использование было небезопасным, поэтому не могу быть уверен, есть ли здесь уязвимость или нет. Возможно, читатели подскажут на этот счёт.
Также к безопасности я отношу возможность удалить личные данные из репозитория (напомню, что у нас очень много коммитов, то есть недостаточно удалить файл из рабочей директории). Жёсткие ссылки здесь скорее в плюс, поскольку мы можем просто вызвать shred для нашего файла, и все его дубликаты будут одновременно стёрты. Затем мы можем удалить файлы с одним путём из всех коммитов, а если сомневаемся, не был ли он в какой-то момент перемещён, то можем найти его по иноду find -inum. Поскольку удалённые коммиты всё равно будут содержать этот файл, то нужно будет также стереть его там с помощью push -f.
Самым главным аспектом безопасности я считаю сохранность данных и всегда вызываю —dry-run перед настоящими push и pull.
$ yarsync push -n dest
покажет, что именно будет перенесено на dest, не делая физических изменений. Если существующий файл был изменён (в результате ошибки или сбоя файловой системы), то это тоже скорее всего будет отражено. Для более надёжной проверки у rsync есть опция checksum (гораздо более длительная, и мне она пока не потребовалась).
ссылка на оригинал статьи https://habr.com/ru/post/662163/
Добавить комментарий