50

What's the easiest way to zip, say 2 files, from a folder on the server and force download? Without saving the "zip" to the server.

$zip = new ZipArchive();
// the string "file1" is the name we're assigning the file in the archive
$zip->addFile(file_get_contents($filepath1), 'file1'); //file 1 that you want compressed
$zip->addFile(file_get_contents($filepath2), 'file2'); //file 2 that you want compressed
$zip->addFile(file_get_contents($filepath3), 'file3'); //file 3 that you want compressed
echo $zip->file(); //this sends the compressed archive to the output buffer instead of writing it to a file.

Can someone verify:

I have a folder with test1.doc, test2.doc, and test3.doc

With the above example - file1 (file2 and file3) might just be test1.doc, etc.

Do I have to do anything with "$filepath1"? Is that the folder directory that holds the 3 docs?

Sorry for my basic question..

hakre
  • 193,403
  • 52
  • 435
  • 836
  • 2
    According to the [documentation](http://us3.php.net/manual/en/class.ziparchive.php) the ZipArchive class has no function file() and this will result in an error. In order to do this on needs a different library like for example [this one](http://stackoverflow.com/questions/1189019/manipulate-an-archive-in-memory-with-php-without-creating-a-temporary-file-on-d). Just putting it here so other people won't spend hours trying to get this to work with the suggestions below. – Neograph734 Sep 18 '14 at 19:15
  • According to the [documentation](https://www.php.net/manual/en/ziparchive.addfile.php) the ZipArchive addFile() method takes a pathname to the file to add, not the contents of that file. The file_get_contents() calls must be removed. The method returns FALSE in case of error, use it for error handling when having an issue with such code, e.g. non-existent files or using the file contents in error. – hakre Aug 04 '23 at 17:55

9 Answers9

83

Unfortunately w/ PHP 5.3.4-dev and Zend Engine v2.3.0 on CentOS 5.x I couldn't get the code above to work. An error message was all I could get:

Invalid or unitialized Zip object

So, in order to make it work, I had to initialize the ZipArchive $zip with a filename as the first parameter of ZipArchive::open() and the ZipArchive::OVERWRITE flag as second parameter.

I successfully used the following snippet (taken from the example by Jonathan Baltazar on PHP.net ZipArchive::open manual page (Jul 2008), with no error-handling for brevity of the example:

// Prepare file
$file = tempnam('/path/to/my/tmp/directory', 'zip');
register_shutdown_function('unlink', $file);
$zip = new ZipArchive();
$zip->open($file, ZipArchive::OVERWRITE);

// Stuff with content
$zip->addFromString('file_name_within_archive.ext', "your string\n");
$zip->addFile('file_on_server.ext', 'second_file_name_within_archive.ext');

// Close and send to users
$zip->close();

header('Content-Type: application/zip');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename="file.zip"');
readfile($file);

I know this is different than working with memory only - unless you have your tmp in ram - but maybe this can help out someone else, who's struggling with the solution above, like I was; and for which this little performance penalty not having a ramdisk for temporary files is not an issue at all.

hakre
  • 193,403
  • 52
  • 435
  • 836
maraspin
  • 2,353
  • 20
  • 16
  • this helped me out today, thanks. when dealing with an admin / cms system that generates PDFs on the fly, performance is secondary to zipping all those files together so I don't have to flood the admin with 15 download dialogues :) – totallyNotLizards Jan 24 '13 at 17:34
  • Does anybody know if there can be any problems with deleting the file right after we read it in the output buffer? Or is that approach bullet proof? – Erik Čerpnjak May 12 '16 at 10:12
9

Your code is very close. You need to use the file name instead of the file contents.

$zip->addFile(file_get_contents($filepath1), 'file1');

should be

$zip->addFile($filepath1, 'file1');

http://us3.php.net/manual/en/function.ziparchive-addfile.php

If you need to add files from a variable instead of a file you can use the addFromString function.

$zip->addFromString( 'file1', $data );
sakabako
  • 1,150
  • 7
  • 14
  • I agree with this but you need to use something like Vinko says with forcing a header in order to get the browser to download. – m4rc Apr 28 '11 at 11:54
7

This works for me (nothing is written to disk)

$tmp_file = tmpfile(); //temp file in memory
$tmp_location = stream_get_meta_data($tmp_file)['uri']; //"location" of temp file

$zip = new ZipArchive;
$res = $zip->open($tmp_location, ZipArchive::CREATE);
$zip->addFile($filepath1, 'file1');
$zip->close();

header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');
echo(file_get_contents($tmp_location));
user2909086
  • 81
  • 1
  • 3
  • tmpfile() creates a fake file in memory only. It's just like a regular file but with no location, and it is deleted as soon as the script ends. stream_get_meta_data($tmp_file)['uri'] gives a provides a path to the fake file for other services to use. With that path you can do anything you would do with a real file. – user2909086 Aug 20 '20 at 22:11
  • 3
    Your comment should be edited into your answer. (Then, of course, delete your comment.) – mickmackusa Sep 29 '20 at 12:54
  • instead of `echo(file_get_contents())` you should use `readfile()` – Clément Moulin - SimpleRezo Nov 23 '21 at 12:14
  • 3
    `tmpfile` is NOT in memory. As i read it depends where you set your temp to be. Default it is in `/tmp/` path. See also: https://stackoverflow.com/a/37574617/3411766 – cottton Jan 16 '22 at 10:52
  • Really thankful for this. It works...nothing gets permanently written to /tmp folder. – Steven Yip Nov 24 '22 at 09:13
6

If you have access to the zip commandline utility you can try

<?php
$zipped_data = `zip -q - files`;
header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');
echo $zipped_data;
?>

where files is the things you want to zip and zip the location to the zip executable.

This assumes Linux or similar, of course. In Windows you might be able to do similar with another compression tool, I guess.

There's also a zip extension, usage shown here.

Community
  • 1
  • 1
Vinko Vrsalovic
  • 330,807
  • 53
  • 334
  • 373
5

maraspin's Answer is what I tried. Strangely, I got it working by removing the header line that references the file size:

header('Content-Length: ' . filesize($file));

With the above change, the code works like a breeze! Without this change, I used to get the following error message:

End-of-central-directory signature not found. Either this file is not a zipfile, or it constitutes one disk of a multi-part archive. In the latter case the central directory and zipfile comment will be found on the last disk(s) of this archive.

Test environment:

OS: Ubuntu 10.10 Browser: Firefox And the default LAMP stack.

Raj
  • 22,346
  • 14
  • 99
  • 142
itsols
  • 5,406
  • 7
  • 51
  • 95
  • 1
    The header is "Content-Length" and the file size, not the length, was being set to it. Maybe that's why? – Parziphal Jan 24 '13 at 03:32
4

To create ZIP files on the fly (in memory), you can use ZipFile class from phpMyAdmin:

An example of how to use it in your own application:

Note: Your ZIP files will be limited by PHP's memory limit, resulting in corrupted archive if you exceed the limit.

dezlov
  • 840
  • 8
  • 20
1

itsols If you want to insert the 'Content-Length' do it like this:

$length = filesize($file);
header('Content-Length: ' . $length);

I don't know why, but it crashes if you put it in the same line.

John Ballinger
  • 7,380
  • 5
  • 41
  • 51
Arkiller
  • 138
  • 1
  • 8
0

On Unix systems (and maybe others),

you can apply a simple trick to @maraspin's answer by deleting the file entry for the file ("unlinking" it from its inode), and send its data using a handle opened previously. This is basically the same thing tmpfile() does; this way you can "temporarify" any file.

The code is the same as maraspin's up to the very last lines:

// Prepare File
$file = tempnam("tmp", "zip");
$zip = new ZipArchive();
$zip->open($file, ZipArchive::OVERWRITE);

// Stuff with content
$zip->addFromString('file_name_within_archive.ext', $your_string_data);
$zip->addFile('file_on_server.ext', 'second_file_name_within_archive.ext');

// Close and send to users
$zip->close();

header('Content-Type: application/zip');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename="file.zip"');

// open a handle to the zip file.
$fp = fopen($file, 'rb');
// unlink the file. The handle will stay valid, and the disk space will remain occupied, until the script ends or all file readers have terminated and closed.
unlink($file);
// Pass the file descriptor to the Web server.
fpassthru($fp);

As soon as the script finishes, or terminates abnormally, or the application pool is cycled, or the Apache child gets recycled -- the "disappearance" of the file will be formalized and its disk space released.

LSerni
  • 55,617
  • 10
  • 65
  • 107
0

This works for me

$tmp = tempnam(sys_get_temp_dir(), 'data');
$zip = new ZipArchive();
$zip->open($tmp, ZipArchive::CREATE);//OVERWRITE    //CREATE
$zip->addFromString('aaa.txt', 'data');//zip on the fly 
$zip->close();
header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="download.zip"');

ob_clean();
flush();

if (file_exists($tmp))
    readfile($tmp);
else
    echo 'No file';
Hakan
  • 240
  • 3
  • 4