Manipulando arquivos ZIP pelo PHP

Artigo que explica como utilizar a extensão ZIP do PHP para criar e manipular pacotes de arquivos, apresentando exemplos de utilização

zip
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.

42 comentários

Anônimo disse...

Bom dia,

não tem um exemplo se o arquivo já existe, como faço para descompactação e subscrever o existente.

Vejo assim:

meu arquivo zipado chama bb.zip dentro tem aa.txt a pasta que ele vai descompactar tem nome de texto dentro desta pasta texto já tem o aa.txt, preciso descompactar o bb.zip na pasta texto e subscrever o lá existente aa.txt.

Copmo faço isso poderia colocar um exemplo, por favor.


Marta

rubS (autor do blog) disse...

Olá, Marta

Dê uma olhada no segundo exemplo de código-fonte deste post. Lá é aberto um arquivo ZIP já existente e é obtido o conteúdo de um arquivo dentro do pacote.

Para modificar o arquivo dentro do pacote ZIP, você pode dar uma olhada no primeiro código-fonte deste post. Basta usar o método addFromString.

Dê uma lidinha com calma, acho que você vai conseguir fazer o que deseja. Qualquer dúvida é só comentar aqui.

Anônimo disse...

Bom dia,


obrigada pelo retorno. Estou tentando ajustar este script para baixar um arquivo.zip de um site.

O arquivo vem vazio sabe porque?


veja o que fiz:


//Baixa o arquivo
$url = "http://www1.caixa.gov.br/loterias/_arquivos/loterias/D_lotfac.zip";
$fh = fopen(basename($url), "wb"); //serve pra abrir streaming do arquivo
$ch = curl_init($url); //instancia o objeto curl
curl_setopt($ch, CURLOPT_FILE, $fh); //seta as opções do curl recém criado
curl_exec($ch); //executa o download
curl_close($ch); //fecha o streaming

rubS (autor do blog) disse...

Para obter o conteúdo do arquivo, acho que seria mais simples fazer assim:

$url = 'http://www ... ';
$conteudo = file_get_contents($url);
file_put_contents('/tmp/arquivo.zip', $conteudo);

Depois, basta usar a classe ZipArchive para abrir o arquivo em "/tmp/arquivo.zip".

Note que para obter o conteúdo da Web, você precisa habilitar a diretiva de configuração "allow_url_fopen".

E para escrever o conteúdo obtido em um arquivo do servidor, precisa garantir que o usuário do servidor HTTP tenha permissão para salvar arquivos no local escolhido (no caso, eu escolhi "/tmp").

Anônimo disse...

Bom dia,


eu mais uma vez, obrigada pela força, seu exemplo está apresentado um erro:

[function.file-get-contents]: failed to open stream: Redirection limit reached, aborting in

O meu baixa o arquivo mas o .zip está vazio. O que devo fazer para baixar o arquivo com tamanho e conteúdo?

Marta

Anônimo disse...

Completando,

dei 777 para a pasta tmp e deu esse erro ainda:

failed to open stream: no suitable wrapper could be found in

Rubens, o meu está baixando só que o arquivo.zip não tem conteúdo, tem tamanho 0 kb


Marta

rubS (autor do blog) disse...

Olá, Marta

O problema é que o servidor da caixa implementou uma medida de "segurança" para evitar que o arquivo seja baixado por um script. Se você usar algum programa como o telnet para simular a requisição HTTP feita ao servidor, notará que o servidor exige a criação de um cookie chamado "security".

Existe uma forma de definir este cookie durante a requisição ao servidor (com o file_get_contents). Porém, se a caixa adotou esta medida, não acho ético divulgar aqui uma forma de burlar. Mesmo que seja extremamente simples.

Sugiro que procure por alguma política de uso dos arquivos da caixa. Depois, se for permitido, dê uma estudada no método file_get_contents para saber como requisitar um arquivo incluindo um cookie.

http://www.php.net/manual/en/function.file-get-contents.php

Anônimo disse...

Boa tarde,


obrigada pela dica, não sabia que era isso, pois estava funcionando até uns 20 dias anteriores, só agora parou de funcionar.

Sendo assim, vou fazer o certo tentar resolver de uma maneira correta junto a caixa e o webmaster de lá.


Parabéns pelo seu trabalho, gostei muito da sua força. Vou indicar seu site para todos.


Um grande abraço,


Marta

Anônimo disse...

Boa tarde,
Obrigada pela dica. :)

Gostei muito desse código.

Bom, só tenho uma dúvida quando eu estava tentando enviar um arquivo pdf para zip, mas vi que não pega o nome do arquivo, você tem alguma ideia?

Atenciosamente
Marina

rubS (autor do blog) disse...

Olá, Marina

Não entendi direito o seu problema, mas vamos lá.

Se você está tentando gravar um arquivo PDF dentro de um pacote ZIP, então precisa especificar o nome do arquivo nos métodos addFromString ou na addFile.

Se você tem um arquivo PDF que está dentro de um pacote ZIP, mas não sabe o nome do arquivo PDF, então você pode percorrer os índices e usar o método statIndex, que uma das informações que ele tem é o nome do arquivo.

Mas se você quiser o caminho absoluto original do arquivo, então esta informação não fica disponível no pacote ZIP, a não ser que você mesma grave esta informação no momento em que gera o pacote ZIP. Isso é feito com "comentários" dentro do pacote ZIP. Existem os métodos "setCommentIndex" e "setCommentName" na classe ZipArchive. E para recuperar um comentário, pode usar "getCommentIndex" ou "getCommentName".

Para mais detalhes, veja lá no manual:
http://php.net/manual/en/book.zip.php

Carlos disse...

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

Olá! Quando o php roda a função header ocorre o seguinte erro:
Warning: Cannot modify header information - headers already sent by (output started at /home/...

Tentei colocar a função ob_start() no inicio do fonte e funcionou mas gera um zip zerado.

Pode me ajudar?

rubS (autor do blog) disse...

Olá Carlos.

Para usar a função header, você não pode ter impresso nada antes (isso inclui espaços e quebras de linha fora do bloco de código .

Antes de chamar a função, inclua estas linhas para saber onde foi impresso alguma coisa indevidamente:

if (headers_sent ($arquivo, $linha)) {
echo 'algo foi impresso pelo arquivo: ' . $arquivo . ' / linha: ' . $linha;
exit(1);
}

Carlos disse...

Obrigado pela resposta.
A questão do header funcionou, mas continua gerando um arquivo zip zerado. No meu localhost está funcionando normal, mas na web não dá certo.
Veja conforme seu texto:
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.

Na permissão da pasta está liberado pra ler/gravar/executar.
Será que tem algo no Apache que deve ser liberado com o provedor de hospedagem?
Agradeço a atenção dispensada.

rubS (autor do blog) disse...

Olá Carlos.

A princípio, acho que só precisa de permissão no diretório e que esteja disponível a extensão ZIP do PHP. Se quiser, mande-me um e-mail com o trecho do script que você está gerando o arquivo que dou uma olhada e te respondo. Meu e-mail está ali no meu perfil, na barra da esquerda.

Carlos disse...

Cara, a sua ajuda será de grande valia.
Tomara que no meu código eu esteja fazendo algo errado.
h t t p : / / p a s t e b i n . c o m / L bt3eJvA

Repito: Gera o arquivo zip normalmente e aparece a tela pra fazer o download mas o arquivo continua zerado.

rubS (autor do blog) disse...

Olá Carlos.

O código parece OK. Se o arquivo é gerado normalmente, então experimente usar outra função diferente de "readfile". Tente:

echo file_get_contents($nome_arq);

Para fazer um teste, você poderia remover as funções header e chamar ver o tamanho do arquivo gerado:

echo filesize($nome_arq);

Ou ainda: verificar se o arquivo existe no momento que é chamado o readfile:

if (is_file($nome_arq)) {
header(...);
readfile(...)
}

Uma outra coisa importante é que $row['chNFe'] não contenha barra (pois indicaria um diretório). Para criar diretórios no arquivo zip, precisa usar um método próprio (veja no post).

Carlos disse...

Meu caro. Brigadão!
Quando mandei o código pra ti, dei uma enxugada no script pois eu havia misturado código com comentários para ir testando. Daí o código que lhe enviei enxuto testei no meu site por ele e deu certo. Acho que devo ter corrigido algo e nem dei conta.
Devo lhe informar que seu blog é um dos me lho res pontos de referência para quem quer programar em php pois tem um texto bem explicado e bem detalhado. E Além do mais responde as nossas questões.

Tudo de bom pra vc!

Anônimo disse...

Por favor, uma dúvida...
estando desabilitado a extensão php_zip.dll no arquivo php.ini, seria possível fazer com que isso funcione via código, ou sei lá alguma diretiva dentro do fonte para fazer com que entenda como se tive habilitado essa extensão ?????

rubS (autor do blog) disse...

Olá, Anônimo

A função "dl" realiza o carregamento dinâmico de extensões em tempo de execução. Porém, ela está depreciada em algumas SAPIs desde o PHP 5.3. Veja a documentação da função em:
php.net/manual/en/function.dl.php

Outra alternativa é encontrar um código PHP que ofereça a mesma classe, mas sem usar recursos da extensão. Por exemplo, uma classe que guarda os arquivos em memória para manipulação e, para gravar ou recuperar os dados, usa os programas "zip" e "unzip".

Anônimo disse...

Bom dia,
Muito bom este artigo,
So tenho uma duvida, se tem como fazer para compactar todo o conteudo de uma pasta, passando apenas o caminho dela, sem passar arquivo por arquivo que tem nela?

rubS (autor do blog) disse...

Olá, Anônimo

Jeito tem, mas não automaticamente. Precisa fazer uma função que busca o conteúdo dos diretórios recursivamente e vai inserindo no zip. Você pode ler o artigo abaixo para montar isso:

http://rubsphp.blogspot.com/2010/11/percorrer-diretorios-e-arquivos.html

Anônimo disse...

Olá,

encontrei um jeito de zipar pasta e sub pastas e seus arquivos, quem estiver interessado:
http://www.web-development-blog.com/archives/tutorial-create-a-zip-file-from-folders-on-the-fly/

Anônimo disse...

Rubens, primeiramente parabpens pelo Post, há muito tempo eu procurava algo a respeito e achei o seu.

Meu nome é MARCELO e tenho umas dúvidas, O arquivo zip eu pretendo abri-lo de servidor via FTP, com usuário e senha e porta, acessando remotamente. E depois eu queria abrir um dos arquivos txt compactados neste zip e inserir o conteúdo deste arquivo txt em tabelas do MYSQL... Estou há dias procurando e não acho no google nem no manual do PHP, vc me ajuda?. Mais uma vez obrigado pela atenção! meu email é marcelo.s.aguiar@hotmail.com

rubS (autor do blog) disse...

Experimente abrir o arquivo desta forma:

$zip->open('ftp://usuario:senha@dominio:porta/arquivo.zip');

Trocando "usuario", "senha", "dominio", "porta" e "arquivo.zip" pelos valores desejados.

Anônimo disse...

É.. tá dando erro 11. Engraçado que no servidor a pasta está com 777 de permissão e no php.ini a variável diretiva allow_url_fopen está ON. Obrigado pela atenção! Att Marcelo

Anônimo disse...

ola tudo bom?EU ABAIXEI UM ARQUIVO ZIP MAIS QUANDO VOU ABRIL ESTA VAZIO..TEM ALGUMA IDEIA..OBRIGADO!

rubS (autor do blog) disse...

Olá Anônimo,
Se o zip foi gerado com a classe, você deve verificar se as chamadas às funções "addFromString" ou "addFile" retornaram "true".

Exemplo:
if (!$zip->addFile(...)) {
echo 'ocorreu algum erro aqui';
}

Talvez você não tenha permissão de acesso ao arquivo ou ele não foi digitado corretamente.

Ou pode ser que você não tenha permissão de escrita no local onde você está salvando o arquivo zip.

Leo Ferronato disse...

Cara mto bom o post..
porém tenho uma dúvida, dentro do arquivo texto criado, como eu posso pular de linha?

Tentei tudo mas não consegui.. exemplo:
$z->addFromString('relatorio.txt', 'anexo1 - texto do anexo1
\n anexo2 - texto do anexo 2');

Isso cria o txt, mas não faz pular de linha..
Você saberia se existe alguma solucução para isso?

lucas amauri disse...

Boa tarde.
No seu segundo post do subtítulo "Lendo e Manipulando um arquivo ZIP dinamicamente" você está utilizando uma variável $z que é inexistente o que ocorrerá um Notice 'Trying to get property of non-object'

guassussenet disse...

Olá. Muito bom o tutorial. Mais não teria como fazer um exemplo de como fazer o upload de um arquivo zip e descompactar online e cadastrar os nomes dos arquivos que estão dentro do zip em um banco de dados?

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, guassussenet

A intenção do artigo era apenas mostrar as instruções relacionadas à compactação e descompactação de arquivos zip.

Para realizar isso que você quer, precisa conhecer um pouco sobre upload, sobre como obter os nomes dos arquivos do pacote zip (veja neste artigo), e sobre como manipular bancos de dados. Dê uma olhada nestes links e depois tente juntar as peças:

http://php.net/manual/en/features.file-upload.post-method.php
http://rubsphp.blogspot.com.br/2010/09/pdo.html

Anônimo disse...

Boa Tarde Rubens, ótimo conteúdo!
Vê se você já passou por isso...
Estou com um problema ao descompactar um zip (erro 19).
Mas o erro só acontece, quando o conteúdo é um diretório.
Quando tento abrir um zip que dentro dele está um arquivo, abre normal.
Quando tento abrir um zip que dentro dele está uma pasta, da erro 19.

Agradeço pela atenção.