Pull to refresh

Работа с архивами tar и gz средствами PHP

Reading time6 min
Views19K
Как это часто случается, все началось с того, что мне потребовалось нечто, позволяющее обрабатывать архивы «tar.gz» средствами php. Покопавшись в Интернете, я с удивлением обнаружил, что ничего сколько-нибудь приемлемого на эту тему не опубликовано.

Что у нас есть?

1. PEAR расширение для PHP http://pear.php.net/package/Archive_Tar Прекрасно, но в моем случае – неприемлемо, поскольку доступа к настройкам сервера у меня нет. Вынужденно отметаем.
2. Отменная статья Алексея Валеева «Работа с архивами tar.gz в php». То, что нужно, но – увы. Мне требовалось решение лицензионно «прозрачное», не способное вызвать вопросов. По этому, использование библиотеки от Битрикса тоже не годилось.

Собственно, это – все.

Дальнейшее просеивание поисковиков ничего разумного не выдало. Немного подумав, я залез в код популярного net2ftp, который, как я помню, прекрасно архивы tar обрабатывает. Выяснилось, что есть на свете библиотека pcltar.lib.php от Vincent Blavet, 2001 года. Лицензия GNU. Все, как надо. Но! Для начала меня смутил размер самой библиотеки 127 килобайт. Ну, есть у меня бзик со старых времен — до сих пор считаю байты. Потом, мне хотелось иметь результат в виде класса, а не отдельными функциями. К тому же, взыграл азарт. Захотелось разобраться досконально.

В результате, пришлось найти описание структуры архива tar (кому интересно, вот тут хорошо расписан заголовок блока информации) и заняться изучением кода. Результат привожу ниже. Понимаю, что задача специфичная, но возможно, кому-то и пригодится.

Итак, как известно, tar архиватором в современном понимании не является. Разработанный для сохранения данных на ленточных носителях, сжимать он не умеет, а просто объединяет множество файлов в один, добавляя свои заголовки, и дополняя получившийся код до ровного количества блоков по 512 байт. Затем результат уже можно сжимать архиватором. Каким? Да, хоть rar-ом. Без разницы. Хотя, традиционно для этого используются форматы gzip и bzip2. Поскольку они-то как раз два файла связать не могут (это диктуется принятой в unix-системах политикой «одна программа – одно действие»). Поддержка gzip и bzip2 в PHP обеспечивается сторонними библиотеками, и нам не важна. Важен сам tar.

Коротко разберем структуру файла. Как и положено, вначале идет заголовок. Изучив документацию, я обнаружил, что есть «старый» и «новый» форматы заголовка. Новый — длиной 512 байт. Получили его, добавив в «старый» дополнительные поля. Теоретически, они совместимы, но мы будем ориентироваться на современность. Попробуем его разобрать. Вот, если коротко, суть:

100 байт name — название (может содержать относительный путь);
8 байт mode file mode
8 байт uid — user ID
8 байт gid — group ID
12 байт size — размер файла, байт (кодирована в восьмеричной системе)
12 байт mtime – дата и время последней модификации в секундах эпохи UNIX (кодирована в восьмеричной системе)
8 байт chksum – контрольная сумма заголовка (не файла!)
1 байт typeflag – определяет файл у нас, или каталог: файл – 0, каталог — 5
100 байт linkname – ссылка на файл
— дальше – поля «нового» формата — 6 байт magic – содержит слово «ustar», т.е. признак «нового» формата
2 байт version – версия нового формата (может отсутствовать)
32 байт uname – имя владельца
32 байт gname – имя группы владельца
8 байт devmajor – старший байт кода устройства
8 байт devminor – младший байт кода устройства
155 байт prefix – префикс (расширение) имени

Не используемые байты должны быть пустыми, хотя допускается код «20» (пробел).

Большая часть этих данных в общем случае не требуется. Лично меня интересовали имя, размер и дата.

Дальше идет собственно информационная часть, дополняемая (внимание!) пустыми байтами до кратного 512 байт. И все заново для следующего файла. Как видим, все просто.

В сущности, этих знаний достаточно, чтобы попробовать запаковать файл.

1. Откроем архив командой fopen(имя_файла).

2. Заголовок. Это самая сложная часть проблемы. Велосипед я изобретать не стал, воспользовавшись функцией из упомянутой библиотеки pcltar.lib.php, слегка её оптимизировав. Приводить здесь весь код не буду из-за объемности, но суть заключается в следующих действиях:
— Определяем имя файла, его размер, дату создания, выставленные на него права. Для каталогов размер указываем нулевым;
— Неиспользуемые параметры объявляем пустыми;
— Численные параметры (размер, дату) переводим в восьмеричную систему;
— Форматируем каждый параметр в соответствии с заявленными размерами соответствующих полей. Здесь есть одна хитрость, с которой я разобрался не сразу – на самом деле, значимая часть каждого поля должна быть на один байт меньше, чем размер самого поля. Последний же байт обязательно должен быть пустым. Иначе архив не читается.
— Пакуем все параметры в две отдельные строки. В две, поскольку между ними должна быть контрольная сумма заголовка.
— Считаем эту контрольную сумму, форматируем по тем же правилам, пакуем.
— А теперь пишем в файл архива последовательно три строки: первую часть параметров, контрольную сумму и вторую часть параметров.

Готово! Вот пример для времени создания:

$mtime = sprintf("%11s ", DecOct(filemtime($filename)));

pack("a100a8a8a8a12a12", …, …, …, …, …, $mtime);


3. С телом файла совсем все просто, Vincent Blavet в своей библиотеке его тоже обрабатывает функцией pack. Но я провел несколько экспериментов с различными файлами и не увидел искажений при паковке / распаковке. По этому, ради выигрыша производительности, делать не стал – нет смысла. Просто читаем данные из файла, разумеется – предварительно его открыв, и пишем в архив. Поскольку размеры файлов в моем случае могли оказаться достаточно большими, делаю я это блоками. Размер блока я принял за 50 Кб.

$infile = fopen($filename, rb);
$j = ceil(filesize($filename) / 51200) + 1;
for($i=0; $i<$j; $i++){
$fr = fread($infile, 51200);
if ($this->tarmode == "tar")
@fputs($this->tarfile, $fr);
else
@gzputs($this->tarfile, $fr);
}
fclose($infile);


4. А теперь добиваем до «ровного». Для этого нам нужно знать сколько байт «не хватило». Если файл меньше 512 байт, то это определяется вычитанием его размера из 512. Если же больше, определяем остаток от деления размера файла на 512, и вычитаем его из 512. Результат пакуем в бинарную строку.

Следует, также, учесть тот случай, когда файл изначально кратен 512 байтам – некоторые программы, самостоятельно дополняют свои файлы до нужного размера. Разумеется, в этом случае ничего не дописывать не требуется.

Вот получившийся код:

$ffs = filesize($filename);
if($ffs > 512)
$tolast = 512 - fmod($ffs, 512);
else
$tolast = 512-$ffs;
if($tolast != 512 && $tolast != 0){
$fdata = pack("a".$tolast, "");
Полученные данные записываем в файл.
}


Результат – архив в формате «tar». Можно теперь повторить операцию со следующим файлом, или закрыть архив.
Если у нас подключена библиотека Zlib, то в процессе создания архив можно сжать, получив в итоге «tar.gz» или «tgz», кому что нравится. Проверить наличие библиотеки проще всего путем проверки константы FORCE_GZIP. Для автоматизации процесса, я ввел такую проверку для всех операций с архивным файлом. Примерно, так:

if(defined('FORCE_GZIP'))
$resopen = @fopen($this->tarname, 'a+b');
else
$resopen = @gzopen($this->tarname, 'a+b'.$this->tarlevel);


На практике я таким образом определяю будущее расширение файла, а уже ориентируясь на него, использую нужные функции, но это не важно.

Остальные операции значительно проще. Поскольку мне не требовались такие функции как удаление файлов из архива, или их поиск, я добавил в свой класс лишь автоматическое определение наличия библиотеки Zlib, о чем написал выше, получение списка файлов и распаковку любого из них. Уже при написании этой статьи мне пришло в голову добавить отдельную функцию полной распаковки архива.

Получить список файлов в архиве можно найдя и прочитав все заголовки. Для этого читаем первые 512 байт архива – это в любом случае будет заголовок и распаковываем его функцией unpack(). Поскольку unpack производит распаковку в ассоциативный массив, заодно присваиваем параметрам понятные наименования. Вот так:

unpack("a100name/a8perms/…и так далее…", “считанные данные”)

Время создания и размер необходимо перевести обратно в десятичное счисление.

Полученные параметры можно отдавать «на выход». Остается только сместить указатель в файле архива на считанный размер запакованного файла плюс остаток до 512-байтного блока. Теперь он показывает на начало следующего заголовка, и операцию можно повторить заново.

Распаковка нужного файла сводится к поиску его заголовка с помощью предыдущей функции, создания в указанном месте файла с полученным именем, перемещении файлового указателя в архиве на начало содержимого файла, считывании количества байт, соответствующих его длине и записи в созданный файл. Для каталогов все ограничивается их созданием.

Единственные две сложности здесь, связаны с особенностями библиотеки Zlib:

Во-первых. Обнаружилось, что в функции gzopen данной библиотеки, не реализован модификант «+» для открытия файла одновременно на запись и на чтение, аналогично функции fopen. Пришлось отказаться от единого открытия / закрытия файла архива, и повторять эти операции при каждом обращении, в соответствии с задачей.

Во-вторых, в документации указано (и я убедился в правдивости этого указания), что функция gzseek, аналогичная fseek «эмулируется, но работает крайне медленно». Пришлось отказаться от прямого смещения указателя в файле архива на нужную позицию, заменив его на «пустое» чтение, в ущерб производительности. Если бы дело ограничивалось только архивами tar, этого можно было бы избежать.

Вот, собственно, и все. В результате, у меня получилась вполне универсальная библиотека, размером чуть больше 11 Кб не сжатого кода. Скачать библиотеку можно здесь: Archivator_tar-tar_gz.zip.

Всегда ваш, PunkerPoock
Tags:
Hubs:
+1
Comments24

Articles