\$\begingroup\$
\$\endgroup\$
6
tmpfile() creates a temporary file which is automatically deleted when php exit. I wanted the same for directories, came up with:
/**
* create a temporary directory that is automatically deleted when the script exits.
*
* @throws RuntimeException on tmpfile()/mkdir() failure
* @return string absolute path to the temporary directory
*/
function tmpdir(): string
{
$max_attempts = 10;
$attempts = 0;
for (; $attempts < $max_attempts; ++$attempts) {
$h = tmpfile();
if ($h === false) {
continue;
}
$tmpDirPath = stream_get_meta_data($h)['uri'];
fclose($h);
// there is technically a (unlikely) race condition between fclose() and mkdir(),
// that's why i do the weird loop thing
if (mkdir($tmpDirPath)) {
break;
}
}
if ($attempts >= $max_attempts) {
throw new RuntimeException("tmpdir() failed: attempts: " . $attempts . ": error_get_last: " . var_export(error_get_last(), true));
}
static $deleteOnExitInstalled = false;
static $tmpDirPaths = [];
$tmpDirPaths[] = $tmpDirPath;
if ($deleteOnExitInstalled === false) {
register_shutdown_function(function () use (&$tmpDirPaths) {
$rmdirRecursively = function ($dir) use (&$rmdirRecursively) {
$children = glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_NOSORT | GLOB_MARK);
foreach ($children as $child) {
$isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;
if ($isChildDir) {
$rmdirRecursively(substr($child, 0, -strlen(DIRECTORY_SEPARATOR)));
} else {
unlink($child);
}
}
rmdir($dir);
};
foreach ($tmpDirPaths as $tmpDirPath) {
$rmdirRecursively($tmpDirPath);
}
});
$deleteOnExitInstalled = true;
}
return $tmpDirPath;
}
sample usage:
$tmpDir = tmpdir();
file_put_contents("{$tmpDir}/test.txt", "Hello World!");
var_dump($tmpDir); // /tmp/phpSyEj2v
asked May 6, 2023 at 8:19
1 Answer 1
\$\begingroup\$
\$\endgroup\$
Found at least 1 bug: if the file contains symlinks, then
$isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;
will incorrectly be true, resulting in the dir not being cleaned up and emitting
PHP Warning: rmdir(/tmp/phpOuCtAg/symlink): Not a directory in foo.php on line x
The fix seems to be
foreach ($children as $child) {
$isChildDir = substr($child, -strlen(DIRECTORY_SEPARATOR)) === DIRECTORY_SEPARATOR;
if ($isChildDir) {
$withoutTrailingSlash = substr($child, 0, -strlen(DIRECTORY_SEPARATOR));
if (is_link($withoutTrailingSlash)) {
unlink($withoutTrailingSlash);
} else {
$rmdirRecursively($withoutTrailingSlash);
}
} else {
unlink($child);
}
}
I think that could probably be shortened further.
Toby Speight
87.3k14 gold badges104 silver badges322 bronze badges
answered Jul 18, 2023 at 11:31
lang-php
--user-data-dir
argument \$\endgroup\$static
with$deleteOnExitInstalled
to control its boolean value after subsequent calls oftmpdir()
. Does$tmpDirPaths
need this same behavior? It cannot start from scratch each time? Why not put$attempts = 0;
in the first parameter offor()
? Why not make$max_attempts = 10
the default parameter oftmpdir()
to make it more easily customizable? Trivially, I'd usesprintf()
with the runtimeException messge to manage the code line length. I might also cache thestrlen()
of the DS. \$\endgroup\$Does $tmpDirPaths need this same behavior? It cannot start from scratch each time?
- yes, the shutdown function use this array to keep track of all the temporary directories created.Why not put $attempts = 0; in the first parameter of for()?
- in PHP it doesn't really matter, but in many other languages (javascript, C, C++, Rust, just off the top of my head) , variables created in the for loop, goes out of scope outside the for loop, so i have a habit of declaring variables used outside for loops, outside the for loop. (even though it's not required in PHP) \$\endgroup\$