Синхронизация файлов с Unison и unisync
Потеряв веру в облачные сервисы некоторое время назад, я обзавёлся скромным домашним сервером, который помимо прочего выполняет функции хранения резервных копий данных (при помощи rsync), обмена файлами (ftp) и синхронизации файлов между компьютерами (Unison). О синхронизации я как раз и расскажу в данной заметке.
Unison
Unison — это программа двунаправленной синхронизации файлов. Двунаправленность в данном случае означает, что одновременные изменения в файлах и каталогах в двух копиях могут быть синхронизированы и объединены. Это и есть основное отличие от зеркалирования при помощи rsync, где изменения распространяются только в одну сторону: от источника к назначению. Unison так же как и rsync передаёт только изменившиеся части файлов.
С помощью Unison можно синхронизировать локальную копию файлов с удалённой или с другой локальной (особенно полезно при отладке). Предпочтительный способ соединения с удалённым компьютером — через ssh. Важно, чтобы на разных компьютерах была установлена одна и та же версия Unison: должны совпадать первые два числа версии, например, 2.40.X.
При синхронизации файлов на более, чем двух компьютерах, рекомендуемая топология — звезда, то есть все синхронизируются через один и тот же компьютер. Это помогает избежать ложных конфликтов, которые могут возникнуть, например, при использовании топологии кольцо.
Опции в unison
передаются в командной строке или посредством профилей —
файлов, содержащих набор опций. В первом случае вызов unison происходит так:
unison root1 root2 [options]
Здесь root1
и root2
указывают на локальный и удалённый пути к файлам или
каталогам. Их порядок не имеет значения. Локальный путь может быть относительным
и абсолютным. Удалённый путь (при использовании ssh) выглядит так:
ssh://user@host//path/of/root
для абсолютного пути или
ssh://user@host/path/of/root
относительно домашнего каталога.
Вызов unison
с указанием профиля происходит следующим образом:
unison profile [options]
Профили хранятся в каталоге ~/.unison
с расширением .prf
(оно упускается
при указании в командной строке). Если в командной строке опции указываются
как -option [xxx]
, то в профиле — option = xxx
. В профиле также доступна
директива include profile
для включения содержимого одного профиля в другой.
Полезные опции
Для указания опций ssh существует sshargs
. Так, если ssh слушает
нестандартный порт, то следует указать -sshargs -pX
. Чтобы активировать
сжатие при передачи данных по ssh — -sshargs -C
.
Если корневой путь (root) указывает на каталог, то по умолчанию синхронизации
подлежит всё его содержимое, включая подкаталоги. При указании одной или
нескольких опций path
синхронизированы будут только данные пути внутри корня.
Отдельные пути внутри корня, которые не следует синхронизировать, можно
установить при помощи опций ignore
.
Такие опции, как ignore
, backup
, merge
и некоторые другие, принимают
шаблон пути определённого формата. Так, указание ignore = Regex .*/mydir/.*
,
ignore = Name *.{txt,org}
, ignore = Path tmp/*
приведёт к игнорированию
файлов, в пути которых содержится каталог mydir, всех файлов с расширением txt и
org и всех файлов в tmp. Полезно знать, что Unison путь к корню представляет в
виде пустой строки, и то, что если каталог попадает под какой-то шаблон
ignore
, то все его содержимое будет проигнорировано тоже.
По умолчанию Unison запускается в интерактивном режиме. Для файлов, у которых
нет конфликтов, то есть файл изменился только на одной стороне, Unison
предлагает «рекомендуемое» действие, то есть перенести изменения на другую
сторону. Пользователь может согласиться или изменить действие (пропустить или
отменить изменения). С опцией -auto
, Unison будет автоматически
синхронизировать файлы, у которых нет конфликтов. При нахождении конфликтов
Unison каждый раз будет запрашивать пользователя. С опцией -batch
Unison
работает в неинтерактивном режиме, синхронизируя все файлы без конфликтов, а
конфликты оставляя неразрешёнными.
Разрешение конфликтов в неинтерактивном режиме
Большинство пользователей ожидает, что процесс синхронизации файлов должен происходить автоматически, работать в фоне и не привлекать пользователя к разрешению конфликтов.
Как уже было сказано, опция -batch
отключает интерактивный режим, но
оставляет конфликты неразрешёнными. На практике это скорее всего значит, что
файл, который однажды изменился на двух сторонах, больше никогда не будет
синхронизирован, а отличия будут только увеличиваться. Представьте, что таким
файлом окажется какая-нибудь sqlite-база данных с историей и закладами
браузера. Не думаю, у кого-то появится желание объединять файлы вручную...
Я вижу несколько способов автоматического разрешения конфликтов, не приводящих к потере данных. У каждого из них есть и преимущества, и недостатки.
-
С использованием опции
force
. Если Unison'у указать-force newer
, то в случае возникновения конфликта будет синхронизирован файл с более поздней датой модификации. Файл с более ранней датой модификации по умолчанию будет перезаписан. Оставить старый файл можно, указав опции создания резервных копий. Вот пример:force = newer # перезаписывать более старые файлы в случае конфликта times = true # синхронизировать время модификации файлов — обязательно backup = Name * # бэкапу подлежат все файлы, кроме указанных через backupnot backup = Name .* # шаблон Name * не включает файлы, начинающиеся с точки backuploc = local # файлы сохранять в том же каталоге, а не в ~/.unison/backup maxbackups = 2 # сохранять до трёх резервных файлов (0..2), более ранние удаляются backupprefix = '.unibak-$VERSION.' # файлы будут начинаться с .unibak-[0-2]
Обращаю внимание, что в командной строке опция backup будет выглядеть как
backup "Name *"
, то естьName *
— это один аргумент. Шаблоны резервных копий автоматически добавляются в список игнорирования, поэтому они по умолчанию не синхронизируются.В данном примере резервная копия будет создаваться не только при возникновении конфликта, как можно было подумать, а при каждом изменении файла в одной из сторон, даже если конфликта нет. Хотя это иногда и полезно — держать несколько последних версий файла, но всё же часто нужно создавать резервную копию только в случае возникновения конфликта.
-
С использованием
merge
. При указании данной опции в случае обнаружения конфликта будет запущена внешняя команда слияния. Слияние файлов разных типов в автоматическом режиме сложно себе представить. Тогда чем она может быть нам полезна? А тем, что внешней командной может быть наш скрипт, вовсе и необязательно выполняющий слияние. Например, по определённому критерию мы можем одну версию файла оставить и синхронизировать, а для другой создать резервную копию. Пример использования опции merge:merge = Regex .* -> unison-merge newer CURRENT1 CURRENT2 NEW
Здесь первая часть выражения до -> указывает фильтр файлов, для которых выполняется слияние, а затем сама команда, а точнее любое выражение, которое может переварить командная оболочка. Специальные слова CURRENT1, CURRENT2, NEW заменяются на пути к локальной версии файла, удалённой версии файла и предполагаемому результату слияния соответственно.
unison-merge — это простой bash-скрипт, который принимает в качестве первого параметра newer, larger, local или remote и оставляет соответственно более новый, больший, локальный или удалённый файл, а для другого создаёт резервную копию по шаблону .bak.дата_изменения.имя. Если в случае использования опции
backup
, резервные копии автоматически игнорируются при синхронизации, то тут надо добавить шаблон вручную, например: -ignore "Name .bak.*".Помимо очевидного преимущества над предыдущим методом, состоящим в том, что резервные копии создаются только для конфликтных файлов, есть и ещё пару. С использованием внешней команды у нас появляется возможность выполнять в ней ещё что-то полезное, например, логирование или уведомление пользователя о конфликтных файлах. Но об этом позже.
Есть у метода и один недостаток. Дело в том, что конфликт возникает в том случае, когда оба файла обновлены и их содержимое не одинаковое. Обновлённым файл считается, если его содержимое изменилось. Казалось бы логично, но это также означает, что если с последней успешной синхронизации на одной машине файл изменился, а на другой изменилась дата модификации, но содержимое осталось прежним, то конфликта нет. Команда слияния не запустится, но и какую версию файла синхронизировать Unison также не будет знать. Если Unison запущен в интерактивном режиме, то он спросит у пользователя, а если в неинтерактивном, то пропустит файл. Случай конечно не частый, но всё же возможный. Я вижу несколько «решений». Первое состоит в простом уведомлении пользователя, который вручную устранит этот псевдоконфликт (ведь всё равно это происходит очень редко). Второй — автоматический: указать
-prefer newer
, таким образом синхронизируется файл с последней датой модификации, но другая версия файла потеряется.
Синхронизация всех файлов
В официальном руководстве приведено четыре возможных варианта синхронизации всех файлов. Вот они:
- Синхронизировать всю домашнюю папку, исключая временные файлы и папки с
помпощью
ignore
. - Выделить одну папку, куда помещать все файлы для синхронизации.
- Выделить одну папку, куда помещать ссылки на файлы и папки. Чтобы Unison
синхронизировал файлы, на которые указывают ссылки, надо ему указать опцию
follow
. - Указать домашнюю папку в качестве корня синхронизации и указывать файлы и
папки относительно корня с помощью
path
.
Варианты 2 и 4 мне кажутся более жизнеспособными, чем 1 и 3, но предпочтительный вариант зависит от ситуации. Лично я не использую ни один из перечисленных, так как у меня для этого есть свой велосипед — unisync. О нём дальше.
unisync
Зачем нужно?
unisync представляет собой простой bash-скрипт, который читает конфигурационный файл, и в зависимости от его содержимого и параметров командной строки вызывает (многократно) Unison с необходимыми опциями.
C unisync можно синхронизировать отдельные файлы и папки, группы файлов или все файлы сразу. При этом нет необходимости задавать хосты и пути к файлам каждый раз, unisync их вычислит на основании вашего местонахождения и заданной конфигурации.
Вся конфигурация полностью задаётся в одном файле ~/.unisyncrc
. По сути
это замена профилям Unison. Хотя их можно использовать и совместно. Для
каждого профиля могут задаваться разные параметры Unison.
Другая полезная особенность unisync состоит в том, что если конфигурационный файл был изменён на одной машине, затем была выполнена синхронизация на другой, то unisync сперва скачает новый конфиг и дальше будет синхронизироваться по нему. В этом случае Unison пришлось бы вызывать два раза.
unisync может оказаться полезным и пользователям, синхронизирующие данные между Unix-подобными ОС и Windows. Так как пути к файлам в данных операционных системах отличаются, то синхронизация всех файлов по методам, предложенным в официальном руководстве, осложнена или невозможна, зато легко осуществима с помощью конфигурации unisync.
Использование
Чтобы синхронизировать все профили из ~/.unisyncrc
, достаточно вызвать
unisync
без параметров:
unisync
Отдельные профили синхронизируются так:
unisync firefox emacs bash
Указание групп, состоящих из множества профилей, происходит точно так же:
unisync configs documents
Получить список всех доступных профилей и групп можно так:
unisync -l unisync -g
Чтобы узнать, с какими параметрами вызывается Unison, нужно указать опцию -d
:
unisync -d [profiles|groups]
Также можно передавать непосредственно параметры Unison:
unisync [profiles|groups] -auto=false -ignore "Name junk"
Если параметр принимает булево значение, такое как auto
или batch
, то
указанное значение можно переопределить, если ещё раз указать данный параметр в
командной строке. В случае с параметрами, которые можно указывать много раз,
такими как ignore
или backup
, то они, не переопределяются, а добавляются к
существующим.
Конфигурирование
Конфигурационный файл .unisyncrc — это тоже bash-файл, который исполняется из unisync в самом начале. Пример данного файла находится в корне репозитория unisync: unisyncrc.example.
Где бы вы не находились — за ноутбуком, домашним компьютером или на работе,
unisync должен вызываться одинаково, адреса и пути синхронизации формируются
автоматически. Для этого в конфигурационном файле установлена переменная
location
в значение $USER@$HOSTNAME
. Значение этого параметра сравнивается
со значениями из списка locations
. Если он равен одному из этих элементов, то
unisync «узнаёт», где вы находитесь, и выбирает по соответствующему индексу
удалённый адрес из remotes
, локальное расположение файлов local_xx
и
удалённое расположение файлов remote_xx
. Если для local_xx
или remote_xx
установлено одно значение, а не список, то полагается, что оно одинаковое для
всех значений locations
и remotes
.
unisync по умолчанию использует одно и тоже ssh-подключение для последовательных
вызовов Unison. Это существенно уменьшает время подключения к удалённому
компьютеру и в случае, если не используется ssh-агент, то пароль запрашивается
единожды. За работу этой функции отвечает параметр ssh_master
, который
соответствует команде, устанавливающей ведущее (master) соединение. Обращаю
внимание, что если вы используете нестандартный порт, то его необходимо указать
здесь, например -p 12345
.
unison_args
устанавливает список параметров, передаваемых Unison при каждом
его вызове. Так в примере активированы неинтерактивный режим, бэкап файлов,
список масок файлов, которые не нужно синхронизировать.
Профили и группы
Для описания одного профиля необходимо указать четыре параметра: profile_xx, local_xx, remote_xx, args_xx. Здесь xx — любая строка. Главное, чтобы она была одной и той же у всех параметров одного профиля. Также по этой строке производится лексикографическая сортировка: при указании всех или группы профилей первыми будут синхронизироваться профили с наименьшим значением данной подстроки.
profile_xx указывает имя профиля, local_xx — локальный путь синхронизации для каждой машины отдельно, remote_xx — путь на удалённой машине, необязательный args_xx — список дополнительных параметров Unison для данного профиля.
Несмотря на то, что корнем синхронизации может быть и отдельный файл, в примере
для таких случаев local_xx указывает на каталог, где находится файл, а сам файл
выбирается опцией -path
. Это было сделано с целью обойти баг Unison, который
проявляется только при активированном бэкапе.
Группа — это список профилей, которому присвоено имя. Группы могут указываться в качестве целей синхронизации, так же как и профили. В данном определении группы
group_configs=(shell hg git)
configs — это имя группы, а shell, hg и git — имена профилей, которые входят в группу.
Указывать профили и группы unisync'у можно вперемешку. Профили могут повторяться (например, в разных группах): unisync всё равно выполнит синхронизацию для таких профилей по одному разу. Порядок, в котором указываются группы и профили, также учитывается.
Пример синхронизации профиля Firefox и Vimperator
Однажды я испытывал сервис Firefox Sync, и в конечном итоге остался разочарованным. На тот момент функции синхронизации расширений и поисковых плагинов ещё не было, что делало синхронизацию для меня малополезной. Но куда больший неудобства доставлял вечно появляющийся диалог ввода пароля. Firefox регулярно обращается к серверу за информацией, а так как пароль учётной записи разблокируется по мастер-паролю на одну сессию, то нужно вводить пароль при каждом открытии браузера, и неважно, что открываться может локальная страница.
В общем, если по каким-то причинам на вашу долю выпало несчастье синхронизировать профиль Firefox'а между Linux и Windows, то указание профиля будет выглядеть примерно следующим образом:
profile_05=firefox_prof local_05=("$HOME/.mozilla/firefox" "$APPDATA/Mozilla/Firefox") remote_05="$SYNC/firefox" args_05=( path profiles.ini path m3tmo7x1.Firefox4/profiles.ini path m3tmo7x1.Firefox4/elemhide.css path m3tmo7x1.Firefox4/patterns.ini path m3tmo7x1.Firefox4/chrome path m3tmo7x1.Firefox4/extensions path m3tmo7x1.Firefox4/gm_scripts path m3tmo7x1.Firefox4/searchplugins path m3tmo7x1.Firefox4/search.sqlite # information about your search plugins path m3tmo7x1.Firefox4/persdict.dat # personal dict path m3tmo7x1.Firefox4/prefs.js path m3tmo7x1.Firefox4/content-prefs.sqlite # Individual settings for pages. path m3tmo7x1.Firefox4/extensions.sqlite # Permission database for cookies, pop-up blocking, image loading and add-ons installation path m3tmo7x1.Firefox4/permissions.sqlite path m3tmo7x1.Firefox4/places.sqlite # Bookmarks and browsing history )
Переменные $SYNC и $APPDATA, разумеется, должны быть определены в конфигурации перед их использованием. Теперь аж целых два профиля для Vimperator:
profile_06=vimperatorrc local_06=("$HOME/.vimperatorrc" "$DOCUMENTS/_vimperatorrc") remote_06="$SYNC/.vimperatorrc" args_06=(-backupnot "Regex [._]vimperatorrc") profile_07=vimperator local_07=("$HOME/.vimperator" "$DOCUMENTS/vimperator") remote_07="$SYNC/.vimperator"
Как видно, конфигурационный файл Vimperator'а имеет разные имена на разных системах, поэтому для него пришлось создать отдельный профиль. Теперь объединим всё это в группу firefox:
group_firefox=(firefox_prof vimperator vimperatorrc)
Уведомления и логирование
Есть несколько случаев, когда имеет смысл уведомлять пользователя о статусе
синхронизации: при ошибке синхронизации и возникновении конфликта. Первое
осуществляется путём анализа кода выхода Unison. Если Unison вышел с ошибкой
и установлена опция sync_error_notify
, то вызывается внешняя команда
уведомления. В примере конфигурации в качестве такой команды приводится
notify-send. Конфликты же можно отследить, если используется внешняя команда
слияния, например, unison-merge
. Для включения уведомлений о конфликтах,
необходимо установить опцию conflict_notify
.
Аналогично, можно создать краткую сводку статусов синхронизации каждого
профиля, установив опцию summary
, а краткий лог конфликтов — с помощью
опции conflict_log
.
Об автоматической синхронизации и паролях
Есть разные подходы организации автоматической синхронизации. Предпочтительный подход может зависеть от нескольких факторов.
Если работа происходит за несколькими компьютерами/устройствами параллельно, то необходимо выполнять синхронизацию регулярно. Тут есть несколько вариантов решения:
- можно добавить задачу синхронизации в cron;
- можно запустить Unison единожды, но указать параметр
-repeat N
, где N — период в секундах, с которым синхронизация должна повторяться; - получать оповещения об изменениях в файловой системе от операционной системы
и запускать синхронизацию данных файлов. Для этого в Unison, начиная с
версии 2.27, для опции
-repeat
можно указать файл, через который Unison'у можно слать имена изменившихся файлов; - в svn проекта Unison есть готовое средство автоматического мониторинга и
синхронизации файлов. Данная возможность доступна при указании параметра
-repeat watch
. Мониторинг файлов реализуется с помощью python-скрипта fsmonitor.py, который требует pyinotify в Linux'е. На момент написания этой заметки данная функция считается экспериментальной.
При постоянной синхронизации очень важно, чтобы время на машинах совпадало с достаточно высокой точностью. Иначе Unison может неправильно определять, где файл был изменён последним. Данную проблему можно решить, установив на каждой машине ntpd.
Если же работа производится на одной машине в один момент времени, то синхронизацию достаточно выполнять в начале и конце сессии.
О паролях
Другая возможная проблема с автоматической синхронизацией — это управление паролями. Если выполнять синхронизацию вручную, то пароль пользователя или пароль к закрытому ключу должен запрашиваться при каждом выполнении программы. При использовании ssh-agent или gpg-agent пароль может запрашиваться один раз на сессию. А если использовать gnome-keyring и добавить пароль в связку login, то расшифровка будет происходить вместе с входом в систему, и таким образом вводить пароль вообще не понадобится. Поэтому я предпочитаю последний вариант.
ssh «знает», что пароль необходимо запрашивать у агента, а не у пользователя,
по установленным переменным окружения, таким как SSH_AUTH_SOCK и
SSH_AGENT_PID. Таким образом, агент должен быть запущен до ssh и быть его
предком. Поэтому при использовании gnome-keyring в качестве ssh-агента, скрипт
синхронизации лучше добавить в систему запуска GNOME: вручную посредством
создания desktop-файла в ~/.config/autostart
или помощью графической утилиты
gnome-session-proprties.
Запуск синхронизации в конце сессии
Запускать синхронизацию в конце сессии необходимо до того, как агент завершит работу. Сначала я предпринял попытку повесить выполнение скрипта на окончание сессии GNOME с использованием dbus-интерфейса, но безрезультатно. Оказалось, что по сигналу EndSession gnome-keyring сам выходит... В конце-концов, я создал скрипт, выполняющий последовательно закрытие всех окон, синхронизацию и выход из системы такого плана:
#!/bin/bash close-all-windows --wait firefox emacs unisync-zenity (( "$?" != 1 )) && gnome-session-quit --power-off
Перед тем, как запустить синхронизацию, желательно закрыть приложения, чьи
профили синхронизируются. Для этого я создал скрипт close-all-windows. При
запуске без параметров, он посылает сигнал закрытия всем окнам и выходит. При
запуске close-all-windows --wait
, скрипт выйдет только тогда, когда все окна
будут закрыты. С такими параметрами close-all-windows --wait firefox emacs
скрипт выйдет тогда, когда все окна с классами firefox и emacs закроются.
В unisync-zenity выполняется синхронизация с визуальной индикацией прогресса и окончательных статусов синхронизации профилей. Если пользователь нажимает OK или проходит некоторое время, появляется диалог выключения системы. При нажатии Esc, диалог закрывается, но система не выключается.
Здесь предполагается, что в ~/.unisyncrc установлен параметр summary=/tmp/unisync_summary. Таким образом, в файл unisync_summary будет записана краткая сводка результатов синхронизации каждого профиля: успешная синхронизация, с ошибками, с пропущенными файлами или с критической ошибкой. Эта информация и отобразится в диалоге, созданном unisync-zenity.
Комментарии