skip to Main Content

In PHP 7.4, neither ZipArchive::setMtimeIndex nor ZipArchive::setMtimeName is available.

When creating Zip archives from files in PHP 7.4; files’ mtime and permissions are applied to the corresponding Zip entries’ attributes by the ZipArchive::addFile method.

Unfortunately, it is not the case with directories; as those are created with the ZipArchive::addEmptyDir method, and not read from the filesystem.

I created a fixDirAttributes function to fix the mtime and permissions for directories within a Zip archive. This cannot work with PHP 7.4 though.

Do you know a way to fix the mtime of a Zip archive’s directories in PHP 7.4?

Here is my code to reproduce the issue:

#!/usr/bin/env php7.4
<?php

if (!file_exists('myDir')) mkdir('myDir');
file_put_contents('myDir/test.txt', 'test');
chmod('myDir/test.txt', 0740);
chmod('myDir', 0750);
touch('myDir/test.txt', mktime(10, 10, 0, 1, 1, 2024));
touch('myDir', mktime(5, 42, 0, 1, 1, 2024));

if (file_exists('test.zip')) unlink('test.zip');
$zip = new ZipArchive();
if ($zip->open('test.zip', ZipArchive::CREATE) === true) {
    $zip->addEmptyDir("myDir");
    fixDirAttributes($zip, 'myDir', 'myDir'); // Directory already existe in current dir ./myDir
    $zip->addFile('myDir/test.txt', 'myDir/test.txt'); // file exist in ./myDir dir
    $zip->close();
    echo "Okn";
} else {
    echo "KOn";
}

function fixDirAttributes(ZipArchive $zip, string $dirPath, string $pathInZip)
{
    $indexInZip = $zip->locateName('/' === mb_substr($pathInZip, -1) ? $pathInZip : $pathInZip . '/');
    if (false !== $indexInZip) {
        if (method_exists($zip, 'setMtimeIndex')) { // PHP >= 8.0.0, PECL zip >= 1.16.0
            $zip->setMtimeIndex($indexInZip, filemtime($dirPath));
        }
        $filePerms = fileperms($dirPath);
        if (false !== $filePerms) { // filePerms supported
            $zip->setExternalAttributesIndex($indexInZip, ZipArchive::OPSYS_DEFAULT, $filePerms << 16);
        }
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    Finally I found a solution that does not involve low level Zip handling and that works with PHP 7.4.

    The trick is to use a timestamp dummy empty file, and use it to create the directory in the Zip Archive.

    Instead of using ZipArchive::addEmptyDir, I use my own addDir implementation that uses the dummy empty file to create the desired directory entry. As a bonus it could also transfer permissions, but changing these in a temporary file might be restricted, so it uses ZipArchive::setExternalAttributesName instead to be safe.

    #!/usr/bin/env php7.4
    <?php
    
    if (!file_exists('myDir')) {
        mkdir('myDir');
    }
    file_put_contents('myDir/test.txt', 'test');
    chmod('myDir/test.txt', 0740);
    chmod('myDir', 0750);
    touch('myDir/test.txt', mktime(10, 10, 0, 1, 1, 2024));
    touch('myDir', mktime(5, 42, 0, 1, 1, 2024));
    
    if (file_exists('test.zip')) {
        unlink('test.zip');
    }
    $zip = new ZipArchive();
    if (false !== $mtimeDummy = tempnam(sys_get_temp_dir(), 'mtimeDummy')) {
        if ($zip->open('test.zip', ZipArchive::CREATE) === true) {
            addDir($zip, 'myDir', 'myDir/');                    // Directory already existe in current dir ./myDir
            $zip->addFile('myDir/test.txt', 'myDir/test.txt');  // file exist in ./myDir dir
            $zip->close();
        }
        unlink($mtimeDummy); // Deletes only after Zip closed
    }
    function addDir(ZipArchive $zip, string $dirPath, string $entryName)
    {
        global $mtimeDummy; // Would be class member in an OOP context
        touch($mtimeDummy, filemtime($dirPath));
        $zip->addFile($mtimeDummy, $entryName); // Add empty dummy as a directory
        if (false !== $filePerms = fileperms($dirPath)) { // filePerms supported
            $zip->setExternalAttributesName($entryName, ZipArchive::OPSYS_UNIX, $filePerms << 16);
        }
    }
    

  2. You can accomplish this with a 3rd party zip library that does not require the native PHP Zip extension.

    composer require nelexa/zip
    
    $zip = new PhpZipZipFile();
    $zip->addFile('myDir/test.txt', 'myDir/test.txt');
    $zip->saveAsFile('test.zip');
    $zip->close();
    

    By default, this library will use the live filesystem attributes, but it yields an archive with no explicit directory entry:

    Archive:  test.zip
     Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
    --------  ------  ------- ---- ---------- ----- --------  ----
           4  Stored        4   0% 2024-01-01 15:10 d87f7e0c  myDir/test.txt
    --------          -------  ---                            -------
           4                4   0%                            1 file
    

    When you unzip the archive, you’d get the correct mtime for the file but the current date/time for the subdir. It seems that’s not what you want however, so you can instead tell the library to explicitly add the separate subdirectory entry, using the live attributes read from the filesystem:

    $zip = new PhpZipZipFile();
    $zip->addSplFile(new SplFileInfo('myDir'));
    $zip->addFile('myDir/test.txt', 'myDir/test.txt');
    $zip->saveAsFile('test.zip');
    $zip->close();
    

    This yields a dedicated subdirectory entry with the desired mtime, and it works the same from 7.4 to 8.3

    Archive:  test.zip
     Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
    --------  ------  ------- ---- ---------- ----- --------  ----
           0  Stored        0   0% 2024-01-01 10:42 00000000  myDir/
           4  Stored        4   0% 2024-01-01 15:10 d87f7e0c  myDir/test.txt
    --------          -------  ---                            -------
           4                4   0%                            2 files
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search