Manipulando arquivos ZIP pelo PHP

Introdução

Existem situações em sistemas de informação Web onde é necessário o envio de um arquivo muito grande ou então vários arquivos de uma vez para o cliente. Talvez uma imagem, talvez um pacote de relatórios, ou simplesmente conjuntos de arquivos. Este artigo mostra como utilizar a extensão Zip do PHP para criar um pacote ZIP (ou ler um pacote existente) e realizar operações sobre ele, tais como incluir arquivos/diretórios para serem compactados, extrair o conteúdo, enviar o pacote para o cliente, etc.


Extensão ZIP

Em PHP, existe uma extensão própria para geração de arquivos ZIP, e ela se chama justamente “Zip“. Ela trabalha com o paradigma OO para manipular arquivos ZIP, tanto para escrita quanto para leitura. A classe principal da extensão é a ZipArchive, que representa um arquivo ZIP.

Mas antes de apresentar o funcionamento da classe, é preciso estar ciente de algumas características de um pacote Zip:

  • O pacote pode armazenar, internamente, arquivos e diretórios como se fosse um sistema de arquivos de um HD;
  • O conteúdo do pacote fica armazenado de forma compactada;
  • Cada elemento do pacote (arquivo ou diretório) possui um nome e um índice único, que podem ser usados para referenciá-los para leitura ou edição.
  • Cada elemento do pacote é independente do outro, portanto, se existe um elemento diretório “teste/” e um elemento arquivo “teste/texto.txt”, é possível apagar o elemento diretório, mas o elemento arquivo continuar existindo. Ou seja, a lógica de funcionamento é diferente de um sistema de arquivos que, ao apagar um diretório, o conteúdo interno é apagado.
  • Cada elemento do pacote pode conter comentários próprios, armazenados no próprio pacote.

Sobre o funcionamento da classe, qualquer operação se inicia criando um objeto (através do construtor que, aparentemente, não recebe parâmetros) e chamando o método open, que serve tanto para especificar o nome do arquivo ZIP a ser criado quanto para especificar um nome de um arquivo ZIP já existente. Após realizar as operações desejadas, chama-se o método close para salvar o arquivo de acordo com o que foi feito no objeto, ou seja, corresponde a uma persistência de dados. Caso este método não seja chamado pelo programador, o PHP o invocará automaticamente no final da execução do script. Portanto, caso tenha sido feita alguma alteração que não se pretende salvar no arquivo, deve-se chamar um método para desfazer as operações (unchangeAll). Observação: ao fechar o arquivo, os elementos são reindexados (começando a partir do zero), portanto, ao ser aberto novamente, o índice de um elemento pode ter mudado.

O objeto do tipo ZipArchive pode ser usado várias vezes. Ou seja, não é necessário criar um objeto para cada arquivo a ser aberto, a não ser que se deseja manipular dois arquivos paralelamente. Caso contrário, basta chamar open e close sucessivamente para abrir e fechar pacotes diferente.

Ao abrir o arquivo com open, pode-se especificar a operação desejada através de uma constante binária para o segundo parâmetro. A constante pode ser a combinação dos seguintes valores:

  • ZipArchive::OVERWRITE – limpa o arquivo aberto, caso ele exista e seja incluído pelo menos um elemento.
  • ZipArchive::CREATE – cria o arquivo com o nome especificado, caso ele não exista, ou carrega o conteúdo do arquivo, caso ele exista (exceto se for executado conjuntamente com a constante EXCL, mostrada abaixo).
  • ZipArchive::EXCL – usada em combinação com CREATE para indicar criação exclusiva, ou seja, só criar o arquivo se ele não existir. Caso o arquivo exista, o método retorna o código do erro e não salva o arquivo.
  • ZipArchive::CHECKCONS – usada para checar a consistência do diretório central com o cabeçalho do arquivo ZIP, onde ficam informações sobre o conteúdo (como se fosse um índice).
  • Não passar nenhuma constante abre o arquivo para leitura/escrita sem apagar o conteúdo carregado inicialmente.

Criando e Manipulando um arquivo ZIP dinamicamente

Para criar um arquivo ZIP dinamicamente, basta chamar o método open com a flag ZipArchive::CREATE. Depois, basta usar os métodos para manipular o objeto:

  • addEmptyDir – Adicionar um elemento diretório a um ponto do pacote.
  • addFromString – Adiciona um elemento arquivo a partir do conteúdo de uma String.
  • addFile – Adiciona um elemento arquivo a partir do conteúdo de um arquivo do HD, ou seja, copia um arquivo do HD para dentro do pacote (podendo mudar o nome durante a cópia).
  • deleteName – Remove um elemento do pacote pelo seu nome.
  • deleteIndex – Remove um elemento do pacote pelo seu índice.

Para manipular os comentários do arquivo ou dos elementos, use os métodos:

  • getArchiveComment – Obtém o comentário do arquivo.
  • getCommentIndex – Obtém o comentário de um elemento pelo seu índice.
  • getCommentName – Obtém o comentário de um elemento pelo seu nome.
  • setArchiveComment – Define o comentário do arquivo.
  • setCommentIndex – Define o comentário de um elemento pelo seu índice.
  • setCommentName – Define o comentário de um elemento pelo seu nome.

Veja um exemplo:


// Criando o objeto $z = new ZipArchive(); // Criando o pacote chamado "teste.zip" $criou = $z->open('teste.zip', ZipArchive::CREATE); if ($criou === true) { // Criando um diretorio chamado "teste" dentro do pacote $z->addEmptyDir('teste'); // Criando um TXT dentro do diretorio "teste" a partir do valor de uma string $z->addFromString('teste/texto.txt', 'Conteúdo do arquivo de Texto'); // Criando outro TXT dentro do diretorio "teste" $z->addFromString('teste/outro.txt', 'Outro arquivo'); // Copiando um arquivo do HD para o diretorio "teste" do pacote $z->addFile('/home/rubens/teste.php', 'teste/teste.php'); // Apagando o segundo TXT $z->deleteName('teste/outro.txt'); // Salvando o arquivo $z->close(); } else { echo 'Erro: '.$criou; }

Lendo e Manipulando um arquivo ZIP dinamicamente

Para realizar a leitura ou manipulação de um arquivo ZIP existente, basta invocar o método open sem nenhuma constante, em seguida usar o método getFromIndex ou getFromName, para obter o conteúdo de um arquivo interno através do seu índice ou do seu nome respectivamente. Veja um exemplo:

// Criando o objeto
$z = new ZipArchive();

// Abrindo o arquivo para leitura/escrita
$abriu = $z->open('teste.zip');
if ($abriu === true) {

    // Obtendo o conteudo de um arquivo pelo nome
    $conteudo_txt = $z->getFromName('teste/texto.txt');

    // Obtendo o conteudo de um arquivo pelo indice
    $conteudo_php = $z->getFromIndex(2);

    // Salvando o arquivo
    $z->close();

} else {
    echo 'Erro: '.$abriu;
}

Porém, nem sempre sabemos os nomes dos elementos de um pacote. Para avaliá-los dinamicamente, podemos usar o atributo interno numFiles, que guarda a quantidade de elementos. Sabendo quantos elementos existem no pacote, podemos percorrer do índice zero até a “quantidade menos um” (último elemento), inclusive obter o nome do elemento com o método getNameIndex, ou informações sobre o arquivo com o método statIndex, conforme o exemplo:

// Criando o objeto
$z = new ZipArchive();

// Abrindo o arquivo para leitura/escrita
$abriu = $z->open('teste.zip');
if ($abriu === true) {

    // Listando os nomes dos elementos
    for ($i = 0; $i < $z->numFiles; $i++) {

        // Obtendo informacoes do indice $i
        $stat = $z->statIndex($i);

        // Obtendo apenas o nome do indice $i
        $nome = $z->getNameIndex($i);

        // Exibindo informacoes do elemento
        echo $stat['name'].PHP_EOL;        // Nome do elemento
        echo $stat['index'].PHP_EOL;       // Indice do elemento
        echo $stat['crc'].PHP_EOL;         // CRC
        echo $stat['size'].PHP_EOL;        // Tamanho original (em bytes)
        echo $stat['mtime'].PHP_EOL;       // Data de modificacao
        echo $stat['comp_size'].PHP_EOL;   // Tamanho compactado (em bytes)
        echo $stat['comp_method'].PHP_EOL; // Metodo de compressao
    }

    // Fechando o arquivo
    $z->close();

} else {
    echo 'Erro: '.$abriu;
}

Observação: para se obter o índice de um elemento a partir do seu nome, basta usar o método locateName informando o nome.


Extraindo o conteúdo de um arquivo ZIP

Para extrair (descompactar/descomprimir) o conteúdo de um pacote ZIP para um diretório, basta chamar o método extractTo. O objeto pode ser tanto um arquivo novo (recém criado) quanto um arquivo já existente e que foi aberto para leitura. O método recebe por parâmetro o diretório onde o conteúdo deve ser extraído e, opcionalmente, um elemento ou array de elementos a serem extraídos (pelo nome). Exemplo:

// Criando o objeto
$z = new ZipArchive();

// Abrindo o arquivo para leitura/escrita
$abriu = $z->open('teste.zip');
if ($abriu === true) {

    // Extraindo todo conteudo no diretorio "/home/rubens/"
    $z->extractTo('/home/rubens/');

    // Extraindo apenas um arquivo no diretório "/tmp/"
    $z->extractTo('/tmp/', array('teste/texto.txt'));

    // Fechando o arquivo
    $z->close();

} else {
    echo 'Erro: '.$abriu;
}

Enviando um pacote ZIP ao cliente

Para enviar um pacote ZIP ao cliente, basta criá-lo com as instruções acima, depois utilizar a função header e readfile para enviar ao cliente, desta forma:

// Criando o arquivo zip com nome "teste.zip"
$z = new ZipArchive();
$z->open('teste.zip');
...
$z->close();

// Enviando para o cliente fazer download
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="teste.zip"');
readfile('teste.zip');
exit(0);

Observações Importantes

Lembre-se que assim como para qualquer outro tipo de arquivo, o usuário usado pelo servidor Web (por exemplo, o usuário “apache”) precisa ter permissão de escrita em um diretório para conseguir criar um arquivo ZIP lá. Da mesma forma, o usuário precisa de permissão de leitura e/ou escrita sobre um arquivo ZIP para realizar as respectivas operações.

Vale relembrar que os elementos do pacote são independentes. Portanto, para apagar um diretório e todo o seu conteúdo, é preciso percorrer os elementos e verificar se o elemento percorrido “pertence” ao diretório. Para isso, basta verificar se o nome do elemento percorrido contém o nome do diretório a ser apagado no seu início (ou seja, se ele tem o diretório como prefixo). Além disso, é preciso garantir que seja usada uma única barra para indicar delimitador de diretórios.

Fonte: https://rubsphp.blogspot.com/2011/03/manipulando-arquivos-zip-pelo-php.html