Наверняка, многие из вас слышали или знают по собственному опыту, что системы контроля версий плохо дружат с бинарными файлами, большими файлами и в особенности — с большими бинарными файлами. Здесь и далее речь идет о современных популярных распределенных системах контроля версий вроде Mercurial и GIT.
Зачастую, это не имеет значения. Уж не знаю, причина это или следствие, но системы контроля версий используются в основном для хранения относительно небольших текстовых файлов. Иногда несколько картинок или библиотек.
Если же проект использует большое количество картинок в высоком разрешении, звуковых файлов, исходных файлов графических, 3D, видео или любых других редакторов, то это проблема. Все эти файлы как правило большие и бинарные, а это означает, что все преимущества и удобства систем контроля версий и хостингов репозиториев со всеми сопутстсвующими сервисами становятся недоступны.
Далее мы на примере рассмотрим интеграцию систем контроля версий и Amazon S3 (облачного хранилища файлов), чтобы использовать преимущества обоих решений и компенсировать недостатки.
Решение написано на C#, использует API Amazon Web Services и показан пример настройки для Mercurial репозитория. Код открыт, ссылка будет в конце статьи. Все написано более или менее модульно, так что добавление поддержки чего-то кроме Amazon S3 не должно составить труда. Могу предположить, что для GIT настроить будет так же легко.
Итак, все началось с идеи — нужна такая программа, которая после интеграции с системой контроля версий и с самим репозиторием работала бы совершенно незаметно, не требуя никаких дополнительных действий от пользователя. Как по волшебству.
Реализовать интеграцию с системой контроля версий можно с помощью так называемых хуков (hook) — событий, которым можно назначить собственные обработчики. Нас интересуют такие, которые запускаются в момент получения или отправки данных в другой репозиторий. У Mercurial нужные хуки называются incoming и outgoing. Соответственно, нужно реализовать по одной команде на каждое событие. Одна для загрузки обновленных данных из рабочей папки в облако, а вторая — для обратного процесса — загрузки обновлений из облака в рабочую папку.
Интеграция с репозиторием осуществляется с помощью файла с метаданными или индексного файла или как вам будет угодно. Этот файл должен содержать описание всех отслеживаемых файлов, как минимум пути к ним. И именно этот файл будет находиться под контролем версий. Сами отслеживаемые файлы будут находиться в .hgignore, списке игнорируемых файлов, иначе пропадает весь смысл этой затеи.
Интеграция с репозиторием
Файл с метаданными выглядит как-то так:
<?xml version="1.0" encoding="utf-8"?>
<assets>
<locations>
<location>ContentTextures</location>
<location>ContentSounds</location>
<location searchPattern="*.pdf">Docs</location>
<location>Reference Libraries</location>
</locations>
<amazonS3>
<accesskey>*****************</accesskey>
<secretkey>****************************************</secretkey>
<bucketname>mybucket</bucketname>
</amazonS3>
<files>
<file path="ContentTexturestexture1.dds" checksum="BEF94D34F75D2190FF98746D3E73308B1A48ED241B857FFF8F9B642E7BB0322A"/>
<file path="ContentTexturestexture1.psd" checksum="743391C2C73684AFE8CEB4A60B0317E634B6E54403E018385A85F048CC5925DE"/>
<!-- И так далее для каждого отслеживаемого файла -->
</files>
</assets>
В этом файле три секции: locations, amazonS3 и files. Первые две настраиваются пользователем в самом начале, а последняя используется самой программой для отслеживания самих файлов.
Locations — это пути, по которым будет осуществляться поиск отслеживаемых файлов. Это либо абсолютные пути, либо пути относительно этого xml файла с настройками. Эти же пути нужно добавить в ignore файл системы контоля версий, чтобы она сама не пыталась их отслеживать.
AmazonS3 — это, как не трудно догадаться, настройки облачного хранилища файлов. Первые два ключа — это Access Keys, которые можно сгенерировать для любого пользователя AWS. Они используются для того, чтобы криптографически подписывать запросы к API Амазона. Bucketname — это имя бакета, сущности внутри Amazon S3, которая может содержать файлы и папки и будет использоваться для хранения всех версий отслеживаемых файлов.
Files настраивать не нужно, так как эту секцию будет редактировать сама программа в процессе работы с репозиторием. Там будет содержаться список всех файлов текущей версии с путями и хешами к ним. Таким образом, когда вместе с pull мы заберем новую версию этого xml файла, то, сравнив содержимое секции Files с содержимым самих отслеживаемых папок, можно понять, какие файлы были добавлены, какие — изменены, а какие — просто перемещены или переименованы. Во время push сравнение выполняется в обратную сторону.
Интеграция с системой контроля версий
Теперь о самих командах. Программа поддерживает три команды: push, pull и status. Первые две предназначены для настройки соответствующих хуков. Status выводит информафию об отслеживаемых файлах и ее вывод похож на вывод hg status — по нему можно понять, какие файлы были добавлены в рабочую папку, изменены, перемещены и каких файлов там не хватает.
Команда push работает следующим образом. Для начала получается список отслеживаемых файлов из xml файла, пути и хеши. Это будет последнее зафиксированное в репозитории состояние. Далее собирается информация о текущем состоянии рабочей папки — пути и хеши всех отслеживаемых файлов. После этого идет сравнение обоих списков.
Здесь может быть четыре разных ситуации:
- Рабочая папка содержит новый файл. Это происходит, когда нет совпадений ни по путяи, ни по хешам. В результате обновляется xml файл, в него добавляется запись о новом файла, а также сам файл загружается в S3.
- Рабочая папка содержит измененный файл. Это происходит, когда есть совпадение по пути, но нет совпадения по хешу. В результате обновляется xml файл, у соответствующей записи изменяется хеш, а в S3 загружается обновленная версия файла.
- Рабочая папка содержит перемещенный или переименованный файл. Это происходит, когда есть совпадение по хешу, но нет совпадения по пути. В результате обновляется xml файл, у соответствующей записи изменяется путь, а в S3 ничего загружать не нужно. Дело в том, что ключем для хранения файлов в S3 является хеш, а информация о пути фиксируется только в xml файле. В данном случае хеш не изменился, поэтому загружать повторно тот же файл в S3 не имеет смысла.
- Отслеживаемый файл бы удален из рабочей папки. Это происходит, когда одной из записей xml файла не соответствует ни один из локальных файлов. В результате эта запись удаляется из xml файла. Из S3 никогда ничего не удаляется, так как основное его назначение — хранить все версии файлов, чтобы можно было откатиться на любую ревизию.
Есть еще пятая возможная ситуация — файл не был изменен. Это происходит, когда есть совпадение и по пути, и по хешу. И никаких действий в этой ситуации предпринимать не требуется.
Команда pull также сравнивает список файлов из xml со списком локальных файлов и работает совершенно аналогично, только в другую сторону. Например, когда в xml содержится запись о новом файле, т. е. нет совпадения ни по пути ни по хешу, то этот файл скачивается из S3 и записывается локально по указанному пути.
Пример hgrc с настроеными хуками:
[hooks]
incoming = pathtoassets.exe pull pathtoassets.config pathtochecksum.cache
outgoing = pathtoassets.exe push pathtoassets.config pathtochecksum.cache
Хеширование
Обращения к S3 сведены к минимуму. Используются только две комманды: GetObject и PutObject. Файл загружается и скачиваестя из S3 только в случае, если это новый либо измененный файл. Это возможно благодаря использованию хеша файла в качестве ключа. Т. е. физически все версии всех файлов лежат в S3 Bucket без всякой иерархии, без папок вообще. Здесь есть очевидный минус — коллизии. Если вдруг у двух файлов будет одинаковый хеш, то информация об одном из них просто не зафиксируется в S3.
Удобство от испльзования хешей в качестве ключа все-таки перевешивает потенциальную опасность, поэтому отказываться от их не хотелось бы. Надо лишь учесть вероятность коллизий, по возможности уменьшить ее и сделать последствия не такими фатальными.
Уменьшить вероятность очень просто — нужно использоваь хеш-функцию с более длинным ключем. В своей реализации я использовал SHA256, чего более чем достаточно. Однако, это все равно не исключает вероятность коллизий. Нужно иметь возможность определять их еще до того, как были сделана какие-либо изменения.
Сделать это также не сложно. Все локальные файлы уже хешируются перед выполнением комманд push и pull. Нужно лишь проверить, нет ли среди хешей совпадений. Достаточно делать проверку во время push, чтобы коллизии не зафиксирофались в репозитории. Если обнаружена коллизия, пользователю выводится сообщение об этой неприятности и предлагается изменить один из двух файлов и сделать push еще раз. Учитывая низкую вероятность возникновения подобных ситуаций, данное решение является удовлетворительным.
Оптимизации
К подобной программе нет жестких требований к производительности. Работает она одну секунду или пять — не столь важно. Однако есть очевидные места, которые можно и нужно учесть. И наверное, самое очевидное — это хеширование.
Выбранный подход предполагает, что во время выполнения любой из комманд нужно вычислить хеши всех отслеживаемых файлов. Эта операция с легкостью может занять минуту или больше, если файлов несколько тысяч или если их суммарный размер больше гигабайта. Вычислять хеши целую минуту — это непростительно долго.
Если заметить, что типичное использование репозитория не продполагает изменение всех файлов сразу перед пушем, то решение становится очевидным — кеширование. В своей реализации я остановился на использовании pipe delimited файла, который бы лежал рядом с программой и содержал информацию о всех прежде вычисленных хешах:
путь к файлу|хеш файла|дата вычисления хеша
Этот файл подгружается перед выполнением команды, используется в процессе, обновляется и сохраняется после выполнения команды. Таким образом, если для файла logo.jpg хеш последний раз был вычислен один день назад, а сам файл последний раз изменялся три дня назад, то повторно вычислять его хеш не имеет смысла.
Также оптмизацией можно с натяжкой назвать использование BufferedStream вместо оригинального FileStream для чтения файлов, в том числе для чтения с целью вычисления хеша. Тесты показали, что использование BufferedStream с размером буфера в 1 мегабайт (вместо стандартного для FileStram 8 килобайт) для вычисления хешей 10 тысяч файлов общим размером более гигабайта ускоряет процесс в четыре раза по сравнению с FileStream для стандартного HDD. Если файлов не так много и они сами по себе размером больше мегабайта, то разница уже не такая существенная и составляет что-то около 5-10 процентов.
Amazon S3
Здесь стоит прояснить два момента. Самый главный — это, вероятно, цена вопроса. Как известно, для новых пользователей первый год использования бесплатный, если не выходить за лимиты. Лимиты следующие: 5 гигабайт, 20000 GetObject запросов в месяц и 2000 PutObject запросов в месяц. Если платить полную стоимость, то месяц будет стоит порядка $1. За это вы получаете резервирование по нескольким датацентрам в пределах региона и хорошие скорости.
Также, смею предположить, что читателя с самого начала мучает следующий вопрос — зачем этот велосипед, если есть Dropbox? Дело в том, что использование Dropbox напрямую для совместной работы чревато — он совершенно не справляется с конфликтами.
Но что, если использовать не напрямую? На самом деле в описанном решении Amazon S3 можно с легкостью заменить на тот же Dropbox, Skydrive, BitTorrent Sync или другие аналоги. В этом случае они будут выступать в качестве хранилища всех версий файлов, а хеши будут использоваться в качестве имен файлов. В моем решении это реализовано через FileSystemRemoteStorage, аналог AmazonS3RemoteStorage.
Обещанная ссылка на исходный код: bitbucket.org/openminded/assetsmanager
Автор: OpenMinded