PHPs ZipArchive drops empty directories - php

Problem
I am building an online file manager, for downloading a whole directory structure I am generating a zip file of all subdirectories and files (recursively), therefore I use the RecursiveDirectoryIterator.
It all works well, but empty directories are not in the generated zip file, although the dir is handled correctly. This is what i am currently using:
<?php
$dirlist = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$filelist = new RecursiveIteratorIterator($dirlist, RecursiveIteratorIterator::SELF_FIRST);
$zip = new ZipArchive();
if ($zip->open($tmpName, ZipArchive::CREATE) !== TRUE) {
die();
}
foreach ($filelist as $key=>$value) {
$result = false;
if (is_dir($key)) {
$result = $zip->addEmptyDir($key);
//this message is correctly generated!
DeWorx_Logger::debug('added dir '.$key .'('.$this->clearRelativePath($key).')');
}
else {
$result = $zip->addFile($key, $key);
}
}
$zip->close();
If I ommit the FilesystemIterator::SKIP_DOTS I end up having a . file in all directories.
Conclusion
The iterator works, the addEmptyDir call gets executed (the result is checked too!) correctly, creating a zip file with various zip tools works with empty directories as intendet.
Is this a bug in phps ZipArchive (php.net lib or am I missing something? I don't want to end up creating dummy files just to keep the directory structure intact.

Related

Adding subfolders to zip file in php (laravel)

I have a function which is working fine to create zip file from folder files. But recently I've had need to add sub-folders into my main folder and now I see my function does not add those sub-folders and files in them into generated zip file.
here is what I have currently:
$zip = new ZipArchive;
if ($zip->open(public_path('Downloads/new_zip.zip'), ZipArchive::CREATE) === TRUE)
{
$files = File::files(public_path('new_zip'), true);
foreach ($files as $key => $value) {
$relativeNameInZipFile = basename($value);
$zip->addFile($value, $relativeNameInZipFile);
}
$zip->close();
}
By using code above, let say I have following structure:
new_zip
sample.txt
It works fine to create zip file for my folder.
But
If my folder structure is like:
new_zip
sample.txt
folder_a
file_a.txt
folder_b
folder_c
file_c.txt
Then it ignores everything from folder_a and beyond.
Any suggestions?
You can use this method
The 1st argument is the path to the directory whose data you want to compress
The 2nd argument is the path to the resulting zip file
for your case:
createZipArchive(public_path('new_zip'), public_path('Downloads/new_zip.zip'))
function createZipArchive(string $sourceDirPath, string $resultZipFilePath): bool
{
$zip = new ZipArchive();
if (true !== $zip->open($resultZipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
return false;
}
/** #var SplFileInfo[] $files */
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDirPath),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
$filePath = $file->getRealPath();
if ($file->isDir() || !$filePath) {
continue;
}
$relativePath = substr($filePath, strlen($sourceDirPath) + 1);
$zip->addFile($filePath, $relativePath);
}
return $zip->close();
}
This method will fully reproduce the folder structure of the source directory.
and a little bit of clarification:
To add the directory "test_dir" and the file "test.txt" to the archive - you just need to do:
$zip->addFile($filePath, "test_dir/test.txt");
The RecursiveDirectoryIterator and RecursiveIteratorIterator are used to recursively traverse the directories of the source folder. They are part of the standard php library. You can read about them in the official php documentation

PHP best way to call loop function multiple time

I have a specific directory which may contain zip files.
I would like to loop through each sub-element of my directory to check if this is a zip. And unzip that. Then process the others files.
I'm using flysystem to work with my files.
So I went for this
$contents = $this->manager->listContents('local://my_directory , true);
foreach ($contents as $file) {
if( $file['extension'] == 'zip')
//Unzip in same location
}
The problem is that the files unziped are not in the loop and if the zip file, contain another zip. The second one will be never be unziped.
So I thought about it
function loopAndUnzip(){
$contents = $this->manager->listContents('local_process://' . $dir['path'] , true);
foreach ($contents as $file) {
if( $file['extension'] == 'zip')
//Unzip and after call loopAndUnzip()
}
}
But the initial function will never be finished and be called over and over if there are zip inside zip.
Isn't it a performance issue?
How to manage this kind of thing?
You can use glob to find them, and make the function recursive. You can do this by starting at a certain dir, unzip all the files into it & check if there are new zips.
I recommend using recursive directories as well. If A.zip and B.zip both have a file called example.txt, it overwrites. With dirs it wont:
function unzipAll(string $dirToScan = "/someDir", $depth=0):void {
if($depth >10 ){
throw new Exception("Maximum zip depth reached");
}
$zipfiles = glob($dirToScan."*.zip");
// Unzip all zips found this round:
foreach ($zipfiles as $zipfile) {
$zipLocation = "/".$zipname;
// unzip here to $zipLocation
// and now check if in the zip dir there is stuff to unzip:
unzipAll($dirToScan.$zipLocation, ++$depth);
}
}
The $depth is optional, but this way you cant zipbomb yourself to death.
loopAndUnzip will do all files again, so you will just again unpack the same zipfile and start over with the entire folder, ad infinitum.
Some possibilities:
Keep a list of items that was already processed or skipped and don't process those again, so while iterating over $contents, keep a separate array, and have something like:
PHP:
foreach ($contents as $file) {
if (!array_search($processedFiles, $file) {
if( $file['extension'] == 'zip')
//Unzip in same location
}
$processedFiles[] = $file;
}
Use an unzipper that returns a list of files/folders created, so you can explicitly process those instead of the full directory contents.
If the unzipper can't do it, you could fake it by extracting to a separate location, get a listing of that location, then move all the files in the original location, and process the list you got.

Linux PHP ExtractTo returns whole path instead of the file structure

I am pulling my hair out over here. I have spent the last week trying to figure out why the ZipArchive extractTo method behaves differently on linux than on our test server (WAMP).
Below is the most basic example of the problem. I simply need to extract a zip that has the following structure:
my-zip-file.zip
-username01
--filename01.txt
-images.zip
--image01.png
-songs.zip
--song01.wav
-username02
--filename01.txt
-images.zip
--image01.png
-songs.zip
--song01.wav
The following code will extract the root zip file and keep the structure on my WAMP server. I do not need to worry about extracting the subfolders yet.
<?php
if(isset($_FILES["zip_file"]["name"])) {
$filename = $_FILES["zip_file"]["name"];
$source = $_FILES["zip_file"]["tmp_name"];
$errors = array();
$name = explode(".", $filename);
$continue = strtolower($name[1]) == 'zip' ? true : false;
if(!$continue) {
$errors[] = "The file you are trying to upload is not a .zip file. Please try again.";
}
$zip = new ZipArchive();
if($zip->open($source) === FALSE)
{
$errors[]= "Failed to open zip file.";
}
if(empty($errors))
{
$zip->extractTo("./uploads");
$zip->close();
$errors[] = "Zip file successfully extracted! <br />";
}
}
?>
The output from the script above on WAMP extracts it correctly (keeping the file structure).
When I run this on our live server the output looks like this:
--username01\filename01.txt
--username01\images.zip
--username01\songs.zip
--username02\filename01.txt
--username02\images.zip
--username02\songs.zip
I cannot figure out why it behaves differently on the live server. Any help will be GREATLY appreciated!
To fix the file paths you can iterate over all extracted files and move them.
Supposing inside your loop over all extracted files you have a variable $source containing the file path (e.g. username01\filename01.txt) you can do the following:
// Get a string with the correct file path
$target = str_replace('\\', '/', $source);
// Create the directory structure to hold the new file
$dir = dirname($target);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
// Move the file to the correct path.
rename($source, $target);
Edit
You should check for a backslash in the file name before executing the logic above. With the iterator, your code should look something like this:
// Assuming the same directory in your code sample.
$dir = new DirectoryIterator('./uploads');
foreach ($dir as $fileinfo) {
if (
$fileinfo->isFile()
&& strpos($fileinfo->getFilename(), '\\') !== false // Checking for a backslash
) {
$source = $fileinfo->getPathname();
// Do the magic, A.K.A. paste the code above
}
}

How to achieve the following file structure when archiving a directory in PHP using ZipArchive();

I'm writing a PHP script that archives a selected directory and all its sub-folders. The code works fine, however, I'm running into a small problem with the structure of my archived file.
Imagine the script is located in var/app/current/example/two/ and that it wants to backup everything plus its sub directories starting at var/app/current
When I run the script it creates an archive with the following structure:
/var/app/current/index.html
/var/app/current/assets/test.css
/var/app/current/example/file.php
/var/app/current/example/two/script.php
Now I was wondering how:
a) How can I remove the /var/app/current/ folders so that the root directory of the archive starts beyond the folder current, creating the following structure:
index.html
assets/test.css
example/file.php
example/two/script.php
b) Why & how can I get rid of the "/" before the folder var?
//Create ZIP file
$zip = new ZipArchive();
$tmpzip = realpath(dirname(__FILE__))."/".substr(md5(TIME_NOW), 0, 10).random_str(54).".zip";
//If ZIP failed
if($zip->open($tmpzip,ZIPARCHIVE::CREATE)!== TRUE)
{
$status = "0";
}
else
{
//Fetch all files from directory
$basepath = getcwd(); // var/app/current/example/two
$basepath = str_replace("/example/two", "", $basepath); // var/app/current
$dir = new RecursiveDirectoryIterator($basepath);
//Loop through each file
foreach(new RecursiveIteratorIterator($dir) as $files => $file)
{
if(($file->getBasename() !== ".") && ($file->getBasename() !== ".."))
{
$zip->addFile(realpath($file), $file);
}
}
$zip->close();
You should try with:
$zip->addFile(realpath($file), str_replace("/var/app/current/","",$file));
I've never used the ZipArchive class before but with most archiver application it works if you change the directory and use relative path.
So you can try to use chdir to the folder you want to zip up.

PHP, different destination adding files to .zip?

Okay let me explain. I have two folders on my server, let's say they're called f1/ and f2/.
Both folders have several files. I'd like to ZipArchive both folders. However, the best I can do is getting the .zip to contain both folders. What I want is to take all the files and folders WITHIN both f1/ and f2/ and archive them, thus having the content of f1/ and f2/ in the root of the .zip, not the two folders.
This is the code I'm currently using, which, like I said, doesn't do what I want:
$zipname = 'ZipArc.zip';
$zip = new ZipArchive;
$zip->open($zipname, ZipArchive::CREATE);
foreach ($array as $file => $value)
{
$zip->addFile("f1/" . $file . ".ini");
}
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator("f2/Data/"));
foreach ($iterator as $key=>$value) {
$zip->addFile(realpath($key), $key);
}
}
$zip->close();
I've searched and searched, but I can't seem to hit the right keywords to find a solution.
Read the docs closer: http://php.net/manual/en/ziparchive.addfile.php
bool ZipArchive::addFile($filename, $localname, ....)
^^^^^^^^^^
so
$zip->addFile('/real/path/on/your/server/file.txt', '/path/within/zip/foo.bar');

Categories