Can Spielt
Can Spielt

Reputation: 27

Preserve the folder structure when creating a Zip archive

I want to create a zip file and copy all the folders and files from a directory to it. It is successfully created and contains the files and folders, but the file tree is not preserved, everything being in the root directory.

My directory:

folder/
    test.txt
    test2.txt
test.php

The zip archive:

folder/
test.txt
test2.txt
test.php

This is my code:

public function createZipFromDir($dir, $zip_file) {
    $zip = new ZipArchive();
    if(true !== $zip->open($zip_file, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE)) {
        return false;
    }

    $this->zipDir($dir, $zip);
    return $zip;
}

public function zipDir($dir, $zip) {
    $dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    $files = scandir($dir);
    foreach($files as $file) {
        if(in_array($file, array('.', '..'))) continue;
        if(is_dir($dir . $file)) {
            $zip->addEmptyDir($file);
            $this->zipDir($dir . $file, $zip);
        } else {
            $zip->addFile($dir . $file, $file);
        }
    }
}

$zip = $this->createZipFromDir($rootPath, $archiveName);

Upvotes: 0

Views: 2378

Answers (1)

spenibus
spenibus

Reputation: 4409

The issue is that when you create a folder or set the localname (second argument of addFile()) when adding a file to the archive, you only use $file, therefore everything gets put at the root. It is necessary to provide the file hierarchy as well.

Now the obvious solution would be to use $dir.$file instead, but this would only work properly on a folder located in the same directory as the script.

We actually need to keep track of two file trees:

  • the real tree, as it exists on the machine
  • the archive tree, relative to the path we want to archive

But since one is just a subset of the other, we can easily keep track of that by splitting the real path in two:

  • $dir, a prefix pointing to the original path
  • $subdir, a path relative to $dir

When referring to a file on the machine, we use $dir.$subdir and when referring to a file in the archive we use only $subdir. This requires us to adapt zipDir() to keep track of the prefix by adding a third argument to it and slightly modifying the call to zipDir() in createZipFromDir().

function createZipFromDir($dir, $zip_file) {
    $zip = new ZipArchive();
    if(true !== $zip->open($zip_file, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE)) {
        return false;
    }

    zipDir(
        // base dir, note we use a trailing separator from now on
        rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR,
        // subdir, empty on initial call
        null,
        // archive ref
        $zip
    );
    return $zip;
}

function zipDir($dir, $subdir, $zip) {

    // using real path
    $files = scandir($dir.$subdir);

    foreach($files as $file) {

        if(in_array($file, array('.', '..')))
            continue;

        // check dir using real path
        if(is_dir($dir.$subdir.$file)) {

            // create folder using relative path
            $zip->addEmptyDir($subdir.$file);

            zipDir(
                $dir,                              // remember base dir
                $subdir.$file.DIRECTORY_SEPARATOR, // relative path, don't forget separator
                $zip                               // archive
            );
        }

        // file
        else {
            // get real path, set relative path
            $zip->addFile($dir.$subdir.$file, $subdir.$file);
        }
    }
}

This code has been tested and is working.

Upvotes: 1

Related Questions