Работа с zip-архивами в PHP

Уже долгие годы самым распространенным форматом сжатия данных, является формат ZIP. Данный формат широко используется в разработках под web. Поэтому многие языки для web-программирования имеют либо встроенные средства или возможности подключения необходимых библиотек для работы с zip-архивами.

Непосредственно в самом PHP функций для распаковки и создания zip-архивов нету. Хотя это зависит, от вариантов его сборки. Но они присутствуют в PHP расширении “php_zip”. И именно оно позволяет работать с архивными zip-файлами.

Распаковка архива

С распаковкой архивов при веб разработке, приходиться сталкиваться наиболее часто, нежели при разработке прикладного ПО. Особенно когда возникает необходимость в пакетной загрузке данных (документы, сертификаты и т.д.). Ведь даже диалоговое окно для открытия файла в браузере, не имеет возможности мульти выбора файлов. Конечно, можно воспользоваться каким-либо flash-загрузчиком, но во многих ситуациях это не подходит. А значит остается всего один вариант – архивация данных. Для работы по распаковке архива есть ряд функций встроенных в расширение php_zip:

  • void zip_close (resource $zip)

    Закрывает архивный zip-файл. Параметр zip обязан быть zip-архивом, открытым до этого функцией zip_open().

  • void zip_entry_close (resource $zip_entry)

    Закрывает вхождение директории, специфицированное параметром zip_entry. Параметр zip_entry обязан быть правильным вхождением директории, открытым функцией zip_entry_open().

  • int zip_entry_filesize (resource $zip_entry)

    Возвращает фактический размер вхождения директории zip_entry. Параметр zip_entry обязан быть правильным вхождением директории, открытым функцией zip_read().

  • string zip_entry_name (resource $zip_entry)

    Возвращает имя вхождения директории zip_entry. Параметр zip_entry обязан быть правильным вхождением директории, открытым функцией zip_read().

  • bool zip_entry_open (resource $zip, resource $zip_entry [, string $mode])

    Открывает вхождение директории в zip-файле для чтения. Параметр zip это правильный дескриптор ресурса, возвращённый функцией zip_open(). Параметр zip_entry это ресурс вхождения директории, возвращённый функцией zip_read(). Необязательный параметр mode может быть одним из режимов, специфицированных в документации для fopen().

    Примечание: в настоящее время mode игнорируется и всегда имеет значение “rb”.Это из-за тог, что zip поддерживается в PHP с доступом только для чтения. Возвращает true при успехе, false при неудаче. В отличие от fopen() и других подобных функций, возвращаемое значение функции zip_entry_open() указывает только на результат операции и не нужно для чтения или закрытия вхождения директории.

  • string zip_entry_read (resource $zip_entry [, int $length])

    Читает до length байтов из открытого вхождения директории. Если параметр length не специфицирован, Функция zip_entry_read() пытается прочитать 1024 байта. Параметр zip_entry является правильным вхождением директории, возвращённым функцией zip_read(). Возвращает прочитанные данные, или false, если достигнут конец файла.

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

  • resource zip_open (string $filename)

    Открывает новый zip-архив для чтения. Параметр filename это имя файла открываемого zip-архива. Возвращает дескриптор ресурса для дальнейшего использования в zip_read() и zip_close(), или возвращает false, если filename не существует.

  • resource zip_read (resource $zip)

    Читает следующее вхождение в файле zip-архива. Параметр zip обязан быть zip-архивом, открытым ранее функцией zip_open(). Возвращает ресурс вхождения директории для дальнейшего использования с zip_entry_… () функциями.

  • int zip_entry_compressedsize(resource $zip_entry)

    Параметр zip_entry обязан быть правильным вхождением директории, открытым функцией zip_read(). Возвращает сжатый размер вхождения директории zip_entry.

  • string zip_entry_compressionmethod (resource $zip_entry)

    Параметр zip_entry обязан быть правильным вхождением директории, открытым функцией zip_read(). Возвращает метод сжатия для вхождения директории zip_entry.

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

function unpackZip($zip_file) {
	$zip_h = zip_open($zip_file);

	if (gettype($zip_h) == 'resource') {
		while($z_entry = zip_read($zip_h)) {
			if ($z_entry) {
				$z_name = zip_entry_name($z_entry);
				$z_size = zip_entry_filesize($z_entry);
				if ($z_size == 0 && $z_name[strlen($z_name) - 1] == '/') {
					mkdir("$z_name", 0775, true);
				} else {
					if (@zip_entry_open($zip_h, $z_entry, 'rb')) {
						$unzip = @fopen("$z_name", 'wb');
						if (gettype($unzip) == 'resource') {
							@fwrite($unzip, zip_entry_read($z_entry,
									$z_size), $z_size);
							@fclose($unzip);
						}
						@chmod("$z_name", 0775);
						@zip_entry_close($z_entry);
					}
				}
			}
		}
		
		zip_close($zip_h);
		
		return true;
	} else {
		return false;
	}
}

unpackZip(dirname ( __FILE__ ) . '/archive.zip');

Выше был продемонстрирован классический пример распаковки архива. Обратите внимание на то, что указанный к архиву путь, должен быть абсолютным. Но тем не менее существует еще одна возможность, чтобы его распаковать. Для этого нужно прибегнуть к помощи методов класса ZipArchive. Этот класс находится все в том же расширении “php_zip”. Итак для того чтобы применить другой вариант распаковки, необходимо написать следующий код:

$zip = new ZipArchive;
if ($zip->open('archive.zip') === TRUE) {
    $zip->extractTo(ROOT_DIR .'/test/');
    $zip->close();
    echo 'Архив распакован';
} else {
    echo 'Не удалось распаковать архив';
}

    Для распаковки архива у данного класса используется только один метод:

  • bool extractTo (string $destination [, mixed $entries ])

    Будущая директория местонахождения распакованного архива задается в параметре $destination. Параметр $entries содержит элементы для извлечения. Он является необязательным и может принимать как одно значение, так и массив значений.

Второй вариант выглядит намного красивее и компактнее, чем первый, не так ли? Поэтому я свой выбор остановил именно на нем. И напоследок протестируем оба варианта на скорость распаковки архива объемом в 205Mb:

Первый вариант:
5.72193 cек
5.99901 cек
5.94039 cек
Второй вариант:
4.82474 cек
5.49326 cек
4.93721 cек

Создание архива

Создание архива происходит сложнее, чем его распаковка. Если конечно требуется создать архив с одним файлом или одной директорией, то здесь все просто. А вот если упаковывать директории с неограниченным уровнем вложенности каталогов, то здесь уже придется немного подумать. Во-первых, необходим хороший рекурсивный алгоритм для обхода директорий. Во-вторых, нужно дополнительно хранить локальное имя файла/каталога. Итак, для создания архива нам понадобятся четыре метода класса ZipArchive:

  • bool addEmptyDir (string $dirname)

    Добавляет в архив пустую директорию. Параметр dirname должен содержать имя директории. Метод в случае успеха возвращает true или false в противном случае.

  • void addFile (string $filename [, string $localname = NULL [, int $start = 0 [, int $length = 0 ]]])

    Добавляет в архив файл, который находится по указанному в параметре filename пути. Параметр localname отвечает за имя файла в архиве. И если он указан, то параметр filename будет переопределен. Параметры start и length, зарезервированы для будущих целей. Данный метод так же в случае успеха возвращает true или false в случае ошибки.

  • void open (string $filename [, int $flags ])

    Данный метод необходим для открытия нового архива с целью: чтения, записи или создания. Параметр filename должен содержать имя архива. Необязательный параметр flags используется в качестве режима открытия файла (ZIPARCHIVE::OVERWRITE, ZIPARCHIVE::CREATE, ZIPARCHIVE::EXCL, ZIPARCHIVE::CHECKCONS). Метод возвращает true в случае успеха или код ошибки (см. предопределенные константы ошибок).

  • void close ()

    Этот метод закрывает открытый или созданный архив и сохраняет изменения. Данный метод автоматически вызывается в конце сценария.

Ниже приведен исходный код созданного класса, позволяющего производить создание zip-архивов:

class Zip extends ZipArchive {

	var $lPrefix; 
	
	function ToZip ($source, $destination){
		$result = false;
		// Получаем директорию исходного каталога или файла
		$dir = pathinfo($destination, PATHINFO_DIRNAME);
		
		// Создаем если необходимо директории для будущего архива
		if (@!file_exists($dir)) {
			if (!mkdir($dir, 0777, true))
				return false;
		}		
		
		// Проверяем существование директории для будущего архива		
		if (@file_exists($dir)) {
			if ($this->open($destination, ZIPARCHIVE::CREATE)) {
								
				/*	
				*	Получаем длину пути исходной директории с
				*	прибавленным одним слешем
				*/
					
				$this->lPrefix = strlen(pathinfo($source,
												  PATHINFO_DIRNAME) . '/');
				
				if (is_dir($source)) {
					/*	
					*	Если файл является директорией, то создаем
					*	структуру каталогов и файлов в будущем архиве
					*/
					
					$this->PackingDirectory($source);
					$result	= true;				
				} else if (is_file($source) && @file_exists($source)) {
					/*	
					*	Если файл является не директорией и существует, то
					*	добавляем его сразу в архив
					*/
					
					$this->addFile($source, pathinfo($source,
													  PATHINFO_BASENAME));
					$result	= true;
				}
				$this->close();
				
			}
		}
		return $result;
	}
	
	function PackingDirectory ($source){
		// Получаем дескриптор обходимого каталога
		$handle = @opendir($source);

		/*	
		*	Совершаем рекурсивный обход всех вложенных
		*	директорий
		*/
		
		while (false !== $node = readdir($handle)) {
			if ($node != '.' && $node != '..') {
				// Полный путь файла, плюс имя узла
				$fPath = "$source/$node";
				
				/*	
				*	Локальный путь файла 
				*	(от исходного каталога)
				*/
				
				$lPath = substr($fPath, $this->lPrefix);
				if (is_dir($fPath)){
				
					/*	
					*	Если файл является каталогом, то 
					*	добавляем в архив категорию
					*/ 
					
					$this->addEmptyDir($lPath);
					$this->PackingDirectory($fPath);
					
				} else {
					// Иначе добавляем файл
					$this->addFile($fPath, $lPath);
				}
			}
		}
		// Закрываем дескриптор обходимого каталога
		closedir($handle);
	}
}

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

  • bool ToZip (string $source, string $destination)

    Создает zip-архив. В параметре source требуется указать путь к каталогу или файлу, который требуется запаковать. А в качестве параметра destination передается имя будущего архива. Метод возвращает true в случае успеха или false в случае возникновения ошибки.

Примеры использования:

$zip = new Zip ();

/*	
*	Добавление в архив директории: /текущая директория/1/
*	имя архива: test.zip
*	Результат: создастся  архив в текущей директории с именем test.zip 
*/
$zip->ToZip(ROOT_DIR . '/1', ROOT_DIR . /'test.zip');

/*	
*	Добавление в архив директории: /текущая директория/1/
*	имя архива: /текущая директория/new_arcive/test.zip
*	Результат: создастся  архив в директории -
*	/текущая директория/new_arcive/ с именем test.zip
*	если директория new_archive не существует, то она будет создана
*/
$zip->ToZip(ROOT_DIR . '/1', ROOT_DIR .'/new_archive/test.zip');

/*	
*	Добавление в архив файла с именем: /текущая директория/test.txt
*	имя архива: /текущая директория/file.zip
*	Результат: создастся  архив file.zip в текущей директории
*/
$zip->ToZip(ROOT_DIR . '/test.txt', ROOT_DIR . '/file.zip');

Предопределенные константы режима работы

ZIPARCHIVE::CREATE (integer) Создавать архив, если он не существует.
ZIPARCHIVE::OVERWRITE (integer) Всегда создавать новый архив, этот режим перезаписывает файлы, если они существуют.
ZIPARCHIVE::EXCL (integer) Выводить ошибку, если архив существует.
ZIPARCHIVE::CHECKCONS (integer) Выполнять дополнительные проверки на структуру архива, и выдавать ошибку при неудаче.

 

Предопределенные константы флагов

ZIPARCHIVE::FL_NOCASE (integer) Игнорировать регистр символов в именах элементов архива.
ZIPARCHIVE::FL_NODIR (integer) Не учитывать пути директорий в архиве.
ZIPARCHIVE::FL_COMPRESSED (integer) Читать сжатые данные.
ZIPARCHIVE::FL_UNCHANGED (integer) Использовать исходные данные, игнорируя изменения.

 

Предопределенные константы методов сжатия

ZIPARCHIVE::CM_DEFAULT (integer) Выбрать лучший метод сжатия deflate или stored (без сжатия).
ZIPARCHIVE::CM_STORE (integer) Метод сжатия stored (без сжатия).
ZIPARCHIVE::CM_SHRINK (integer) Метод сжатия shrunk.
ZIPARCHIVE::CM_REDUCE_1 (integer) Метод сжатия reduced with factor 1.
ZIPARCHIVE::CM_REDUCE_2 (integer) Метод сжатия reduced with factor 2.
ZIPARCHIVE::CM_REDUCE_3 (integer) Метод сжатия reduced with factor 3.
ZIPARCHIVE::CM_REDUCE_4 (integer) Метод сжатия reduced with factor 4.
ZIPARCHIVE::CM_IMPLODE (integer) Метод сжатия imploded.
ZIPARCHIVE::CM_DEFLATE (integer) Метод сжатия deflated.
ZIPARCHIVE::CM_DEFLATE64 (integer) Метод сжатия deflate64.
ZIPARCHIVE::CM_PKWARE_IMPLODE (integer) Метод сжатия PKWARE imploding.
ZIPARCHIVE::CM_BZIP2 (integer) Метод сжатия алгоритмом BZIP2

 

Предопределенные константы ошибок

ZIPARCHIVE::ER_OK (integer) Нет ошибок.
ZIPARCHIVE::ER_MULTIDISK (integer) Многотомный ZIP архив не поддерживается.
ZIPARCHIVE::ER_RENAME (integer) Переименование временного файла не удалось.
ZIPARCHIVE::ER_CLOSE (integer) Закрытие ZIP архива не удалось.
ZIPARCHIVE::ER_SEEK (integer) Ошибка поиска.
ZIPARCHIVE::ER_READ (integer) Ошибка чтения.
ZIPARCHIVE::ER_WRITE (integer) Ошибка записи.
ZIPARCHIVE::ER_CRC (integer) Ошибка контрольной суммы.
ZIPARCHIVE::ER_ZIPCLOSED (integer) Открытый ZIP архив был закрыт.
ZIPARCHIVE::ER_NOENT (integer) Нет такого файла.
ZIPARCHIVE::ER_EXISTS (integer) Файл уже существует.
ZIPARCHIVE::ER_OPEN (integer) Невозможно открыть файл.
ZIPARCHIVE::ER_TMPOPEN (integer) Не удалось создать временный файл.
ZIPARCHIVE::ER_ZLIB (integer) Ошибка Zlib.
ZIPARCHIVE::ER_MEMORY (integer) Ошибка выделения памяти.
ZIPARCHIVE::ER_CHANGED (string) Запись была изменена.
ZIPARCHIVE::ER_COMPNOTSUPP (integer) Метод сжатия не поддерживается.
ZIPARCHIVE::ER_EOF (integer) Преждевременный конец файла.
ZIPARCHIVE::ER_INVAL (integer) Недопустимый аргумент.
ZIPARCHIVE::ER_NOZIP (integer) Не ZIP архив.
ZIPARCHIVE::ER_INTERNAL (integer) Внутренняя ошибка.
ZIPARCHIVE::ER_INCONS (integer) ZIP архив несовместим.
ZIPARCHIVE::ER_REMOVE (integer) Невозможно удалить файл.
ZIPARCHIVE::ER_DELETED (integer) Запись была удалена.