phpdirectoryphp-ziparchivefilemtime

Set directory mtime in ZipArchive with PHP 7.4


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 "Ok\n";
} else {
    echo "KO\n";
}

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);
        }
    }
}

Solution

  • 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);
        }
    }