php - extract files from folder in a zip - php

I have a zip file containing one folder, that contains more folders and files, like this:
myfile.zip
-firstlevel
--folder1
--folder2
--folder3
--file1
--file2
Now, I want to extract this file using PHPs ZipArchive, but without the "firstlevel" folder. At the moment, the results look like this:
destination/firstlevel/folder1
destination/firstlevel/folder2
...
The result I'd like to have would look like this:
destination/folder1
destination/folder2
...
I've tried extractTo, which produces the first mentioned result, and copy(), as suggested here, but this doesn't seem to work at all.
My current code is here:
if($zip->open('myfile.zip') === true) {
$firstlevel = $zip->getNameIndex(0);
for($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);
$pos = strpos($entry, $firstlevel);
if ($pos !== false) {
$file = substr($entry, strlen($firstlevel));
if(strlen($file) > 0){
$files[] = $file;
}
}
}
//attempt 1 (extractTo):
//$zip->extractTo('./test', $files);
//attempt 2 (copy):
foreach($files as $filename){
copy('zip://'.$firstlevel.'/'.$filename, 'test/'.$filename);
}
}
How can I achieve the result I'm aiming for?

Take a look at my Quick Unzipper script. I wrote this for personal use a while back when uploading large zip files to a server. It was a backup, and 1,000s of files take forever with FTP so using a zip file was faster. I use Git and everything, but there wasn't another option for me. I place this php file in the directory I want the files to go, and put the zip file in the same directory. For my script, they all have to operate in the same directory. It was an easy way to secure it for my needs, as everything I needed was in the same dir.
Quick Unzipper: https://github.com/incomepitbull/QuickUnzipper/blob/master/unzip.php
I linked the file because I am not showcasing the repo, just the code that makes the unzip tick. With modern versions of PHP, there should't be anything that isn't included on your setup. So you shouldn't need to do any server config changes to use this.
Here is the PHP Doc for the ZipArchive class it uses: http://php.net/manual/en/class.ziparchive.php
There isn't any included way to do what you want, which is a shame. So I would unzip the file to a temp directory, then use another function to copy the contents to where you want. So when using ZipArchive, you will need to return the first item to get the folder name if it is unknown. If the folder is known, ie: the same pesky folder name every time, then you could hard code the name.
I have made it return the first item from the index. So if you ALWAYS have a zip with 1 folder inside it, and everything in that folder, this would work. However, if you have a zip file without everything consolidated inside 1 folder, it would fail. The code I have added will take care of your question. You will need to add further logic to handle alternate cases.
Also, You will still be left with the old directory from when we extract it to the temp directory for "processing". So I included code to delete it too.
NOTE: The code uses a lot of if's to show the processing steps, and print a message for testing purposes. You would need to modify it to your needs.
<?php
public function copyDirectoryContents($source, $destination, $create=false)
{
if ( ! is_dir($source) ) {
return false;
}
if ( ! is_dir($destination) && $create === true ) {
#mkdir($destination);
}
if ( is_dir($destination) ) {
$files = array_diff(scandir($source), array('.','..'));
foreach ($files as $file)
{
if ( is_dir($file) ) {
copyDirectoryContents("$source/$file", "$destination/$file");
} else {
#copy("$source/$file", "$destination/$file");
}
}
return true;
}
return false;
}
public function removeDirectory($directory, $options=array())
{
if(!isset($options['traverseSymlinks']))
$options['traverseSymlinks']=false;
$files = array_diff(scandir($directory), array('.','..'));
foreach ($files as $file)
{
if (is_dir("$directory/$file"))
{
if(!$options['traverseSymlinks'] && is_link(rtrim($file,DIRECTORY_SEPARATOR))) {
unlink("$directory/$file");
} else {
removeDirectory("$directory/$file",$options);
}
} else {
unlink("$directory/$file");
}
}
return rmdir($directory);
}
$file = dirname(__FILE__) . '/file.zip'; // full path to zip file needing extracted
$temp = dirname(__FILE__) . '/zip-temp'; // full path to temp dir to process extractions
$path = dirname(__FILE__) . '/extracted'; // full path to final destination to put the files (not the folder)
$firstDir = null; // holds the name of the first directory
$zip = new ZipArchive;
$res = $zip->open($file);
if ($res === TRUE) {
$firstDir = $zip->getNameIndex(0);
$zip->extractTo($temp);
$zip->close();
$status = "<strong>Success:</strong> '$file' extracted to '$temp'.";
} else {
$status = "<strong>Error:</strong> Could not extract '$file'.";
}
echo $status . '<br />';
if ( empty($firstDir) ) {
echo 'Error: first directory was empty!';
} else {
$firstDir = realpath($temp . '/' . $firstDir);
echo "First Directory: $firstDir <br />";
if ( is_dir($firstDir) ) {
if ( copyDirectoryContents($firstDir, $path) ) {
echo 'Directory contents copied!<br />';
if ( removeDirectory($directory) ) {
echo 'Temp directory deleted!<br />';
echo 'Done!<br />';
} else {
echo 'Error deleting temp directory!<br />';
}
} else {
echo 'Error copying directory contents!<br />';
}
} else {
echo 'Error: Could not find first directory';
}
}

Related

PHP Not Showing System Files/MAC Files

I have a php script that uploads a zip file of images to a folder. This script recursively seeks out only files in the zip file and places all the files in a single directory on the server.
The problem is certain files get double uploaded. This is not the script's fault, but rather, due to the ridiculousness and inferiority of apple computers, when a mac creates a zip file of images it creates a folder of the images and then another folder with the exact same images only it places "._" in front of the file names. So seeing as how we're not going to be blessed with the disappearance of apple computers anytime soon, I tried to include in my php script a simple function to search for these inferior mac abominations and delete them from the directory. However, php isn't even pulling these files when I use "ftp_nlist".
So my question is: How do I get php to pull these stupid things so I can delete them?
$contents = ftp_nlist($conn_id, '.');
foreach($contents as $key => $value){
echo $key." => ".$value."<BR>";
if(substr($value, 1, 1) == ".") {
if(ftp_delete($conn_id, $value)) {
echo "Deleting $value<BR>";
}
}
echo "<BR>";
}
exit();
EDIT:
So thanks to Stephane's suggestion I was able to come up with this which works
if($zip->open($_FILES['theFile']['tmp_name']) === TRUE){
for($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
$fileinfo = pathinfo($filename);
copy("zip://".$_FILES['theFile']['tmp_name']."#".$filename, $ezPresenter['currentFolder'].'/'.$fileinfo['basename']);
}
$zip->close();
}else{
exit("Could not upload/extract file");
}
$contents = ftp_rawlist($conn_id, '-a');
foreach($contents as $key => $value){
$value = explode(" ", $value);
$value = $value[count($value)-1];
echo $key." => ".$value."<BR>";
if(strpos($value, ".") === false) {
if(ftp_delete($conn_id, $value)) {
echo "Deleting $value<BR>";
}
}
if(substr($value, 0, 2) == "._") {
if(ftp_delete($conn_id, $value)) {
echo "Deleting $value<BR>";
}
}elseif(substr($value, 0, 1) == "." && $value != "." && $value != "..") {
if(ftp_delete($conn_id, $value)) {
echo "Deleting $value<BR>";
}
}
}
Use ftp_rawlist instead.
ftp_rawlist — Returns a detailed list of files in the given directory
ftp_rawlist($connid, "-a");
Argument -a means all as on unix command-line: ls -a.
I ran into this issue before, but I wasn't using ftp_nlist. What I ended up doing was using PHP's ZipArchive to open the zip file and look for (and exclude) the __MACOSX directory. I also ignored zip files where just one directory was inside (so you don't unzip the file and then have two directories deep to get to the data - that always annoys me).
My solution may not be the best for you, as it takes some extra processing, but it worked for me :)
Anyway, without further ado... here is the code I am using. Hopefully it'll be of help to you:
//
// unzip the file
$zip = new ZipArchive;
if ($zip->open($fname) === TRUE) {
//extract zip
$zip->extractTo($dir);
$zip->close();
//detect single dir
$basedir = function($x) use (&$basedir) {
$files = glob($x.'*', GLOB_MARK);
//ignore stupid mac directory
$k = array_search($x.'__MACOSX/',$files);
if($k!==FALSE) {
unset($files[$k]);
$files = array_values($files);
}
if(sizeof($files)==1 && is_dir($files[0]))
return $basedir($files[0]);
return $x;
};
//get root directory that has files in it
$dir = substr($basedir($dir.'/'),0,-1);
//
// here I re-zipped the data from the base directory
// and uploaded this file
//
} else {
//delete the file
unlink($fname);
//
// some other error handling
//
return;
}

ZipArchives stores absolute paths

Can I zip files using relative paths?
For example:
$zip->addFile('c:/wamp/www/foo/file.txt');
the ZIP should have a directory structure like:
foo
-> file.txt
and not:
wamp
-> www
-> foo
-> file.txt
like it is by default...
ps: my full code is here (I'm using ZipArchive to compress contents of a directory into a zip file)
See the addFile() function definition, you can override the archive filename:
$zip->addFile('/path/to/index.txt', 'newname.txt');
If you are trying to recursively add all subfolders and files of a folder, you can try the code below (I modified this code/note in the php manual).
class Zipper extends ZipArchive {
public function addDir($path, $parent_dir = '') {
if($parent_dir != ''){
$this->addEmptyDir($parent_dir);
$parent_dir .= '/';
print '<br>adding dir ' . $parent_dir . '<br>';
}
$nodes = glob($path . '/*');
foreach ($nodes as $node) {
if (is_dir($node)) {
$this->addDir($node, $parent_dir.basename($node));
}
else if (is_file($node)) {
$this->addFile($node, $parent_dir.basename($node));
print 'adding file '.$parent_dir.basename($node) . '<br>';
}
}
}
} // class Zipper
So basically what this does is it does not include the directories (absolute path) before the actual directory/folder that you want zipped but instead only starts from the actual folder (relative path) you want zipped.
Here is a modified version of Paolo's script in order to also include dot files like .htaccess, and it should also be a bit faster since I replaced glob by opendir as adviced here.
<?php
$password = 'set_a_password'; // password to avoid listing your files to anybody
if (strcmp(md5($_GET['password']), md5($password))) die();
// Make sure the script can handle large folders/files
ini_set('max_execution_time', 600);
ini_set('memory_limit','1024M');
//path to directory to scan
if (!empty($_GET['path'])) {
$fullpath = realpath($_GET['path']); // append path if set in GET
} else { // else by default, current directory
$fullpath = realpath(dirname(__FILE__)); // current directory where the script resides
}
$directory = basename($fullpath); // parent directry name (not fullpath)
$zipfilepath = $fullpath.'/'.$directory.'_'.date('Y-m-d_His').'.zip';
$zip = new Zipper();
if ($zip->open($zipfilepath, ZipArchive::CREATE)!==TRUE) {
exit("cannot open/create zip <$zipfilepath>\n");
}
$past = time();
$zip->addDir($fullpath);
$zip->close();
print("<br /><hr />All done! Zipfile saved into ".$zipfilepath);
print('<br />Done in '.(time() - $past).' seconds.');
class Zipper extends ZipArchive {
// Thank's to Paolo for this great snippet: http://stackoverflow.com/a/17440780/1121352
// Modified by LRQ3000
public function addDir($path, $parent_dir = '') {
if($parent_dir != '' and $parent_dir != '.' and $parent_dir != './') {
$this->addEmptyDir($parent_dir);
$parent_dir .= '/';
print '<br />--> ' . $parent_dir . '<br />';
}
$dir = opendir($path);
if (empty($dir)) return; // skip if no files in folder
while(($node = readdir($dir)) !== false) {
if ( $node == '.' or $node == '..' ) continue; // avoid these special directories, but not .htaccess (except with GLOB which anyway do not show dot files)
$nodepath = $parent_dir.basename($node); // with opendir
if (is_dir($nodepath)) {
$this->addDir($nodepath, $parent_dir.basename($node));
} elseif (is_file($nodepath)) {
$this->addFile($nodepath, $parent_dir.basename($node));
print $parent_dir.basename($node).'<br />';
}
}
}
} // class Zipper
?>
This is a standalone script, just copy/paste it into a .php file (eg: zipall.php) and open it in your browser (eg: zipall.php?password=set_a_password , if you don't set the correct password, the page will stay blank for security). You must use a FTP account to retrieve the zip file afterwards, this is also a security measure.

Create a ZIP file script not creating ZIPs (no errors)

I'm not a native PHP developer but I make do with what I can find and hack together out there, so please excuse me if this doesn't make too much sense:
I have a simple (so it seems) script that takes two arguments in the URL. One is a simple string (title of the ZIP to be created) and the other is a serialized array of audio tracks that point to the files on the server that need zipping. These then pass just fine and are unserialized etc. Here's the script:
<?php
$audioTitleVar = unserialize(rawurldecode($_GET['audioTitle']));
$audioArrayVar = unserialize(rawurldecode($_GET['audioArray']));
function create_zip( $files = array(), $destination = '', $overwrite = true ) {
if(file_exists($destination) && !$overwrite) { return false; }
$valid_files = array();
if(is_array($files)) {
foreach($files as $file) {
if( file_exists($_SERVER['DOCUMENT_ROOT'] . str_replace("http://mydomain.com","", $file)) ) {
$valid_files[] = $_SERVER['DOCUMENT_ROOT'] . str_replace("http://mydomain.com","", $file);
}
}
}
if(count($valid_files)) {
$zip = new ZipArchive();
if($zip->open($destination,$overwrite ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== true) {
return false;
}
foreach( $valid_files as $file ) {
$just_name = preg_replace("/(.*)\/?([^\/]+)/","$2",$file);
$zip->addFile($file,$just_name);
//$zip->addFile($file,$file);
}
//echo '<p>The zip archive contains ' . $zip->numFiles . ' files with a status of ' . $zip->status . '</p>';
$zip->close();
return file_exists($destination);
} else {
return false;
}
}
$fileName = $_SERVER['DOCUMENT_ROOT'] . '/wp-content/themes/jones/zip/' . $audioTitleVar . '.zip';
create_zip( $audioArrayVar, $fileName, true );
//echo '<p>File Path: ' . $fileName . '</p>';
var_dump(file_exists($fileName));
?>
I guess my real issue here, is that, although the script DOES NOT error...no ZIP is created. I have, over time placed outputs in the function at certain parts to see if it was even getting there, and it is - so I'm stumped on this.
What I really need is for one of you guys to scan the script over to see if there's anything glaringly obvious that just won't work!
Could it be a permissions thing? Should PHP be running in CGI mode or Apache? Or does this not make a difference?
The bones of this script were taken from: http://davidwalsh.name/create-zip-php but it's never worked even with that version alone.
PS - I'd just like to add, that if I uncomment the line that tells me how many files are in the ZIP etc, it seems to return the correct info...but the final check on whether the file exists returns false.
Ok, I have finally got it working.
Seems there was something in the addFile() that corrupted the code.
I changed this:
foreach( $valid_files as $file ) {
$just_name = preg_replace("/(.*)\/?([^\/]+)/","$2",$file);
$zip->addFile($file,$just_name);
//$zip->addFile($file,$file);
}
To this:
foreach( $valid_files as $file ) {
// Use basename to get JUST the file name from the path.
$zip->addFile($file, basename($file) );
}
The basename function gets only the file NAME to then place in the ZIP. Before it was the whole path on the server, and this was causing Windows to choke on it.
Hope this helps someone someday.

move all files in a folder to another?

when moving one file from one location to another i use
rename('path/filename', 'newpath/filename');
how do you move all files in a folder to another folder? tried this one without result:
rename('path/*', 'newpath/*');
A slightly verbose solution:
// Get array of all source files
$files = scandir("source");
// Identify directories
$source = "source/";
$destination = "destination/";
// Cycle through all source files
foreach ($files as $file) {
if (in_array($file, array(".",".."))) continue;
// If we copied this successfully, mark it for deletion
if (copy($source.$file, $destination.$file)) {
$delete[] = $source.$file;
}
}
// Delete all successfully-copied files
foreach ($delete as $file) {
unlink($file);
}
Please try this solution, it's tested successfully ::
<?php
$files = scandir("f1");
$oldfolder = "f1/";
$newfolder = "f2/";
foreach($files as $fname) {
if($fname != '.' && $fname != '..') {
rename($oldfolder.$fname, $newfolder.$fname);
}
}
?>
An alternate using rename() and with some error checking:
$srcDir = 'dir1';
$destDir = 'dir2';
if (file_exists($destDir)) {
if (is_dir($destDir)) {
if (is_writable($destDir)) {
if ($handle = opendir($srcDir)) {
while (false !== ($file = readdir($handle))) {
if (is_file($srcDir . '/' . $file)) {
rename($srcDir . '/' . $file, $destDir . '/' . $file);
}
}
closedir($handle);
} else {
echo "$srcDir could not be opened.\n";
}
} else {
echo "$destDir is not writable!\n";
}
} else {
echo "$destDir is not a directory!\n";
}
} else {
echo "$destDir does not exist\n";
}
tried this one?:
<?php
$oldfolderpath = "old/folder";
$newfolderpath = "new/folder";
rename($oldfolderpath,$newfolderpath);
?>
So I tried to use the rename() function as described and I kept getting the error back that there was no such file or directory. I placed the code within an if else statement in order to ensure that I really did have the directories created. It looked like this:
$tempDir = '/home/site/images/tmp/';
$permanentDir = '/home/site/images/' . $claimid; // this was stored above
mkdir($permanentDir,0775);
if(is_dir($permanentDir)){
echo $permanentDir . ' is a directory';
if(is_dir($tempDir)){
echo $tempDir . ' is a directory';
}else{
echo $tempDir . ' is not a directory';
}
}else{
echo $permanentDir . ' is not a directory';
}
rename($tempDir . "*", $permanentDir);
So when I ran the code again, it spit out that both paths were directories. I was stumped. I talked with a coworker and he suggested, "Why not just rename the temp directory to the new directory, since you want to move all the files anyway?"
Turns out, this is what I ended up doing. I gave up trying to use the wildcard with the rename() function and instead just use the rename() to rename the temp directory to the permanent one.
so it looks like this.
$tempDir = '/home/site/images/tmp/';
$permanentDir = '/home/site/images/' . $claimid; // this was stored above
mkdir($permanentDir,0775);
rename($tempDir, $permanentDir);
This worked beautifully for my purposes since I don't need the old tmp directory to remain there after the files have been uploaded and "moved".
Hope this helps. If anyone knows why the wildcard doesn't work in the rename() function and why I was getting the error stating above, please, let me know.
Move or copy the way I use it
function copyfiles($source_folder, $target_folder, $move=false) {
$source_folder=trim($source_folder, '/').'/';
$target_folder=trim($target_folder, '/').'/';
$files = scandir($source_folder);
foreach($files as $file) {
if($file != '.' && $file != '..') {
if ($move) {
rename($source_folder.$file, $target_folder.$file);
} else {
copy($source_folder.$file, $target_folder.$file);
}
}
}
}
function movefiles($source_folder, $target_folder) {
copyfiles($source_folder, $target_folder, $move=true);
}
try this:
rename('path/*', 'newpath/');
I do not see a point in having an asterisk in the destination
If the target directory doesn't exist, you'll need to create it first:
mkdir('newpath');
rename('path/*', 'newpath/');
As a side note; when you copy files to another folder, their last changed time becomes current timestamp. So you should touch() the new files.
... (some codes for directory looping) ...
if (copy($source.$file, $destination.$file)) {
$delete[] = $source.$file;
$filetimestamp = filemtime($source.$file);
touch($destination.$file,$filetimestamp);
}
... (some codes) ...
Not sure if this helps anyone or not, but thought I'd post anyway. Had a challenge where I has heaps of movies I'd purchased and downloaded through various online stores all stored in one folder, but all in their own subfolders and all with different naming conventions. I wanted to move all of them into the parent folder and rename them all to look pretty. all of the subfolders I'd managed to rename with a bulk renaming tool and conditional name formatting. the subfolders had other files in them i didn't want. so i wrote the following php script to, 1. rename/move all files with extension mp4 to their parent directory while giving them the same name as their containing folder, 2. delete contents of subfolders and look for directories inside them to empty and then rmdir, 3. rmdir the subfolders.
$handle = opendir("D:/Movies/");
while ($file = readdir($handle)) {
if ($file != "." && $file != ".." && is_dir($file)) {
$newhandle = opendir("D:/Movies/".$file);
while($newfile = readdir($newhandle)) {
if ($newfile != "." && $newfile != ".." && is_file("D:/Movies/".$file."/".$newfile)) {
$parts = explode(".",$newfile);
if (end($parts) == "mp4") {
if (!file_exists("D:/Movies/".$file.".mp4")) {
rename("D:/Movies/".$file."/".$newfile,"D:/Movies/".$file.".mp4");
}
else {
unlink("D:/Movies/".$file."/".$newfile);
}
}
else { unlink("D:/Movies/".$file."/".$newfile); }
}
else if ($newfile != "." && $newfile != ".." && is_dir("D:/Movies/".$file."/".$newfile)) {
$dirhandle = opendir("D:/Movies/".$file."/".$newfile);
while ($dirfile = readdir($dirhandle)){
if ($dirfile != "." && $dirfile != ".."){
unlink("D:/Movies/".$file."/".$newfile."/".$dirfile);
}
}
rmdir("D:/Movies/".$file."/".$newfile);
}
}
unlink("D:/Movies/".$file);
}
}
i move all my .json files from root folder to json folder with this
foreach (glob("*.json") as $filename) {
rename($filename,"json/".$filename);
}
pd: someone 2020?

PHP Directory Listing Code Malfunction

I tried to write a script to list all files in directories and subdirectories and so on.. The script works fine if I don't include the check to see whether any of the files are directories. The code doesn't generate errors but it generates a hundred lines of text saying "Directory Listing of ." instead of what I was expecting. Any idea why this isn't working?
<?php
//define the path as relative
$path = "./";
function listagain($pth)
{
//using the opendir function
$dir_handle = #opendir($pth) or die("Unable to open $pth");
echo "Directory Listing of $pth<br/>";
//running the while loop
while ($file = readdir($dir_handle))
{
//check whether file is directory
if(is_dir($file))
{
//if it is, generate it's list of files
listagain($file);
}
else
{
if($file!="." && $file!="..")
echo "<a href='$file'>$file</a><br/>";
}
}
//closing the directory
closedir($dir_handle);
}
listagain($path)
?>
The first enties . and .. refer to the current and parent directory respectivly. So you get a infinite recursion.
You should first check for that before checking the file type:
if ($file!="." && $file!="..") {
if (is_dir($file)) {
listagain($file);
} else {
echo ''.htmlspecialchars($file).'<br/>';
}
}
The problem is, variable $file contains only basename of path. So, you need to use $pth.$file.

Categories