Про работу в php с архивами 1

Материал из DOM

Перейти к: навигация, поиск

[править] Работа в php с архивами gzip и zip 1

Переезд дело муторное, хоть в мире реальном, хоть в мире виртуальном. Прошло уже почти три недели с того, как я сменил хостинг и перенес сайт, но до сих пор я нахожу ошибки. Причем там где, казалось бы, ошибки в принципе не могли возникнуть. Найдя очередной “баг” и исправив его, решил написать маленькую заметку (по теме) про php и zip-архивы.

Есть у меня на сайте такой сервис: “архив исходных текстов + подсветка ”. Я выкладываю не только статьи с небольшими примерами исходного кода, но и довольно большие фрагменты (десяток файлов разбитых на каталоги). Очевидно, что помещать их в, собственно, текст страницы (статьи) глупо. Можно было бы поместить их в архив zip|rar, снабдить парой комментариев и благополучно “забыть”. Но, как известно у настоящего программиста должны быть три благодетели:

Лень, гордыня, нетерпеливость

Поэтому я решил поработать над собой и написать небольшой плагин для mediawiki, который бы умел показывать файловое дерево со списком файлов и подкаталогов. Я создал каталог sources, внутрь которого поместил подпапки для всех проектов с исходниками. Теперь для того чтобы в текст страницы был вставлено подобное файловое дерево, я пишу в тексте страницы нечто вроде (параметр base на картинке указывает относительный от корня хранилища путь к папке с исходниками):

Изображение:sources_fix_1.png 

Жму сохранить и вижу содержимое каталога php/wget.

Изображение:sources_fix_2.png

А по нажатию на файл открывалась бы страница с исходным текстом этого файла (если это текстовой документ). Исходный код был бы подсвечен, например, с помощью geshi. Если жмут на файл картинки, то открывается сама картинка.

Ага, и тут я решил изобрести велосипед!

Точно, файловых менеджеров вагон и маленькая тележка, взять хотя бы phpXplorer или … Стоп, но это отдельный продукт (как его встроить в mediawiki ни малейшего представления), и, скажем прямо, его функциональность избыточна, а если что-то избыточно, то и вредно. Одним словом, плагин я написал. Полюбовавшись на алеповатый интерфейс и походив по каталогам вверх-вниз, я догадался, что получилось то неплохо, но неудобно. Нужна возможность загрузить весь архив со связанными ресурсами за один клик. Давайте разберемся, как в php реализована работа с архивами.

Есть три возможных формата при работе с архивами: gz, zip, bz2. Вообще то можно добавить поддержку любого архиватора (даже без знания c|c++ и умения писать расширения для php) – это вызвать из php команду shell, передав архиватору нужные для его работы параметры. Вам поможет одна из следующих функций:

$XXX_CMD = ‘конструируем строку, запускающую архиватор с некоторым набором параметров командной строки’;
// “настоящие” программисты должны данные, из которых конструируется строка для 
// исполнения shell предварительно проверить и экранировать опасные символы с помощью 
// escapeshellcmd или escapeshellarg
system($XXX_CMD);// можно и с помощью других функций
exec($XXX_CMD);// использование внешнего приложения может быть наилучшим способом 
// выйти из проблемы, если данные подлежащие архивации достаточно велики
shell_exec($XXX_CMD);

Найдется еще пара способов запуска внешнего приложения, но разговор все же будет именно о встроенной поддержке архивации в php. Давным-давно (во времена php версии 4.3) в состав php была встроена поддержка архивов gzip. Для работы с архивом нужно открыть его (можно открыть как для чтения так и для записи), при открытии следует указать в качестве параметра режим (чтение или запись), а также степень сжатия. В следующем примере (аккуратно выдранном из официальной справки) открывается для записи файл “somefile.gz”. Режим архивируемой информации равен 9-и, максимальный. Затем в файл пишется немного текста, и файл закрывается.

<?php
// сохраняем в файл некоторую строку текста
$gz = gzopen('somefile.gz','w9');
gzputs ($gz, 'I was added to somefile.gz');
gzclose($gz);
?>

Для того, чтобы прочитать содержимое архива вы выполняете почти те же самые действия: открыть, прочитать и закрыть:

<?php
// получаем содержимое gz-файла в виде строки длиной до 10000 символов
$filename = "/usr/local/something.txt.gz";
$zd = gzopen($filename, "r");
$contents = gzread($zd, 10000);
gzclose($zd);
?>

Вторым параметром функции gzread следует указать то, сколько байт следует прочитать из файла (если файл кончится раньше, то ничего страшного). Фактически если посмотреть на перечень доступных функций для работы с архивами gzip, то возникнет ощущение дежа-вю. Когда вы работали с обычными файлами, то использовали функции открытия, закрытия, чтения и записи содержимого в файл и их имена были…

ОписаниеОбычные файлыАрхивы gzip
Открыть файл для чтения или записиFopengzopen
Закрыть файл после завершения работыFcloseGzclose
Прочитать произвольное количество байт из файлаFreadGzread
Проверить, что в файле остались еще не прочитанные данныеFeofGzeof
Записать в файл информациюFwriteGzwrite
И многие и многие другие

И для того чтобы окончательно вас запутать разработчики php ввели понятие wrapper-ов или handler-ов, которые могут играть роль посредников при работе с файлами. Например если вы откроете файл так:

$h = fopen (‘boo.txt’, ‘r’);

То файл будет прочитан без каких-либо модификаций (архив это или не архив, да хоть картинка) вы получите те самые биты и байты, из которых файл и состоит. А вот если добавить перед именем файла указание протокола, то правила игры меняются.

$h = fopen (‘http://abracadabra.ru/file.html’, ‘r’);

Теперь мы будем читать содержимое файла размещенного на другом сервере http. А так мы будем читать файл с сервера ftp

$h = fopen (‘ftp://vasyano:secret@comp.server.ru’, ‘r’);

А вот мы прочитали содержимое файла gzip да еще и расположенного на ftp-сервере.

$h = file_get_contents("compress.zlib://ftp://vasyano:secret@center/fex/backup/black-zorro-com_2007-12-08_00-57.sql.gz", "r");
// функция file_get_contents так же как и fopen умеет работать с wrapper-ами

Одним словом, из wrapper-ов можно строить цепочки и результат работы одного из них подавать в качестве исходных данных для другого.

Можно создать собственный wrapper (если это вам интересно, то “копайте” в направлении функции stream_wrapper_register), так чтобы научиться прозрачно читать, скажем, архивы rar (правда, не понятно зачем, но это дело другое).

Супер, скажите вы. Значит я могу легко добавить к своему менеджеру исходников функцию архивации файлов и загрузки их клиентом. Точно и выглядит это примерно так:

// предполагается, что в переменной $fullname находится имя файла,
// которое нужно заархивировать и отправить клиенту
header("Content-type: application/gzip");
header('Content-Disposition: attachment; filename="'. basename($fullname) . '.zip' . '"');
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
header("Pragma: public");
die (gzencode (file_get_contents($fullname), 7));

Содержимое файла было прочитано в память с помощью функции file_get_contents, затем оно поступило на вход функции gzencode которая и выполнила сжатие. Цифра 7 указывает на степень сжатия, ее возможные значения от 0 до 9. 0 – сжатие не выполняется, а 9-ка, соответственно, задает максимальное сжатие. На практике разница между получаемыми после сжатия файлами в режимах 7,8,9 не велика, а вот процессорное время следует экономить.

Итак я научился архивировать файл, но … только один файл. Увы, особенность работы gz функций в том, что сжатию может быть подвергнут один и только один файл, а я ведь хочу иметь возможность поместить в архив содержимое каталога со всеми вложенными в него подкаталогами и файлами. Так что мне пришлось продолжить свой “квест”. Работа с множеством вложенных файлов доступна, например, в старом добром формате zip. Давайте посмотрим, что есть в php и нам может помочь.

На старом хостинге (jino-net.ru) версия php была 5.2.0, в ее состав входит поддержка работы с архивами zip с помощью класса ZipArchive. Далее идет пример кода из справки php, показывающий как можно создать архива и поместить в него несколько файлов.

<?php
// создаем объект Архив (внутри его находится множество функций для чтения и записи zip-файлов)
$zip = new ZipArchive();
$filename = "./test112.zip";
// открываем (точнее создаем новый файл архива) 
// указывая имя файла и режим его открытия (CREATE)
if ($zip->open($filename, ZIPARCHIVE::CREATE)!==TRUE) {
   exit("cannot open <$filename>\n");
// открыть файл не удалось, так что завершим работу скрипта аварийно
}
// теперь начинаем наполнять архив содержимым
$zip->addFromString("testfilephp.txt" . time(), "#1 This is a test string added as testfilephp.txt.\n");
$zip->addFromString("testfilephp2.txt" . time(), "#2 This is a test string added as testfilephp2.txt.\n");
$zip->addFile($thisdir . "/too.php","/testfromfile.php");
echo "numfiles: " . $zip->numFiles . "\n";
echo "status:" . $zip->status . "\n";
$zip->close();
?>

Когда мы добавляем в архив файлы, то можем воспользоваться следующими двумя приемами: добавление существующего файла размещенного на диске, или же в архив помещается новый файл, но его содержимое следует указать явно в виде строки при вызове функции добавления. В первом случае используется функция addFile, а во втором – addFromString.

Теперь я смог написать код формирующий архив с содержимым некоторого каталога. Предполагается, что в переменной $fullpath хранится путь к каталогу, который нужно заархивировать.

$zip = new ZipArchive();
// генерируем случайное имя файла размещенного в каталоге temp – именно внутрь этого файла будет помещен архив
$tmpfname = tempnam(null, 'alldirshhhc');
if ($zip->open($tmpfname, ZIPARCHIVE::OVERWRITE)!==TRUE) {
	return false;
}
$zip->addFromString("readme_zip.txt", "This file is autogenerated, and contains directory content");
rec_scan_dir_to_zip ( $fullpath, $zip, ‘’ );
$zip->close();
 
header("Content-type: application/zip");
header('Content-Disposition: attachment; filename="'.  (basename($fullpath)) . '.zip' . '"');
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
header("Pragma: public");
$h = fopen ($tmpfname, 'rb');
$fc = fread ($h , filesize ($tmpfname));
fclose($h);
die ($fc);

Непосредственно процесс формирования архива реализован внутри функции rec_scan_dir_to_zip. Она будет рекурсивно сканировать каталог (первый параметр функции) и помещать все найденные файлы в объект-архив (задан вторым параметром). Значение третьего параметра (он равен пустой строке) вы лучше поймете, когда проанализируете следующий фрагмент кода:

// функция, выполняющая архивирование данного каталога и всех его подкаталогов в архив zip
function rec_scan_dir_to_zip($dir, $zip, $fromname){
  $dir = realpath ($dir . '/' ) . '/';
  $h = opendir ($dir);
  if (! $h) { 
      /*print 'cannot open:' . $dir ;*/
      return false;
  }
  while (($file = readdir ($h))!==false){
    $fullname = $dir . $file;
    if ($file == '..' || $file == '.') 
        continue;
    if (! is_readable ($fullname) ) continue;
    if (is_dir ($fullname)){
      rec_scan_dir_to_zip ($fullname , $zip, $fromname . $file . ‘/’);
    }
    else{
       $locfilename = $fromname . $file;
        $zip->addFile($fullname, $locfilename);
    }// if file or dir
}// while
closedir ($h);
}// if open dir

Обратите внимание только на строку, где я непосредственно добавляю в архив очередной файл. Я должен указать при вызове функции addFile два параметра – имя файла в файловой системе сервера (именно содержимое этого файла будет прочитано, заархивировано и помещено внутрь архива zip), а также “короткое имя” – имя под которым файл будет помещен внутрь архива. Очевидно, что короткое имя должно отсчитываться от корня самого первого каталога, который нужно заархивировать.

Subscribe Now!

 

ObMachine projects & articles (java, flash, flex, php, ...)  -- black-zorro.com