Making a temporary dir for unpacking a zipfile into

So I first found a post by Ron Korving on PHP.net, which I then modified to make a bit safer (from endless loops, invalid characters, and unwritable parent dirs) and use a bit more entropy.

<?php
/**
 * Creates a random unique temporary directory, with specified parameters,
 * that does not already exist (like tempnam(), but for dirs).
 *
 * Created dir will begin with the specified prefix, followed by random
 * numbers.
 *
 * @link https://php.net/manual/en/function.tempnam.php
 *
 * @param string|null $dir Base directory under which to create temp dir.
 *     If null, the default system temp dir (sys_get_temp_dir()) will be
 *     used.
 * @param string $prefix String with which to prefix created dirs.
 * @param int $mode Octal file permission mask for the newly-created dir.
 *     Should begin with a 0.
 * @param int $maxAttempts Maximum attempts before giving up (to prevent
 *     endless loops).
 * @return string|bool Full path to newly-created dir, or false on failure.
 */
function tempdir($dir = null, $prefix = 'tmp_', $mode = 0700, $maxAttempts = 1000)
{
    /* Use the system temp dir by default. */
    if (is_null($dir))
    {
        $dir = sys_get_temp_dir();
    }

    /* Trim trailing slashes from $dir. */
    $dir = rtrim($dir, DIRECTORY_SEPARATOR);

    /* If we don't have permission to create a directory, fail, otherwise we will
     * be stuck in an endless loop.
     */
    if (!is_dir($dir) || !is_writable($dir))
    {
        return false;
    }

    /* Make sure characters in prefix are safe. */
    if (strpbrk($prefix, '\\/:*?"<>|') !== false)
    {
        return false;
    }

    /* Attempt to create a random directory until it works. Abort if we reach
     * $maxAttempts. Something screwy could be happening with the filesystem
     * and our loop could otherwise become endless.
     */
    $attempts = 0;
    do
    {
        $path = sprintf('%s%s%s%s', $dir, DIRECTORY_SEPARATOR, $prefix, mt_rand(100000, mt_getrandmax()));
    } while (
        !mkdir($path, $mode) &&
        $attempts++ < $maxAttempts
    );

    return $path;
}
?>

So, let's try it out:

<?php
echo "\n";
$dir1 = tempdir();
echo $dir1, "\n";
var_dump(is_dir($dir1), is_writable($dir1));
var_dump(rmdir($dir1));

echo "\n";
$dir2 = tempdir('/tmp', 'stack_');
echo $dir2, "\n";
var_dump(is_dir($dir2), is_writable($dir2));
var_dump(rmdir($dir2));

echo "\n";
$dir3 = tempdir(null, 'stack_');
echo $dir3, "\n";
var_dump(is_dir($dir3), is_writable($dir3));
var_dump(rmdir($dir3));
?>

Result:

/var/folders/v4/647wm24x2ysdjwx6z_f07_kw0000gp/T/tmp_900342820
bool(true)
bool(true)
bool(true)

/tmp/stack_1102047767
bool(true)
bool(true)
bool(true)

/var/folders/v4/647wm24x2ysdjwx6z_f07_kw0000gp/T/stack_638989419
bool(true)
bool(true)
bool(true)

quite easy (I took partly it from the PHP manual):

<?php

function tempdir() {
    $tempfile=tempnam(sys_get_temp_dir(),'');
    // tempnam creates file on disk
    if (file_exists($tempfile)) { unlink($tempfile); }
    mkdir($tempfile);
    if (is_dir($tempfile)) { return $tempfile; }
}

/*example*/

echo tempdir();
// returns: /tmp/8e9MLi

See: https://www.php.net/manual/en/function.tempnam.php

Please look at Will's solution below.

=> My answer should not be the accepted answer anymore.


Another option if running on linux with mktemp and access to the exec function is the following:

<?php

function tempdir($dir=NULL,$prefix=NULL) {
  $template = "{$prefix}XXXXXX";
  if (($dir) && (is_dir($dir))) { $tmpdir = "--tmpdir=$dir"; }
  else { $tmpdir = '--tmpdir=' . sys_get_temp_dir(); }
  return exec("mktemp -d $tmpdir $template");
}

/*example*/

$dir = tempdir();
echo "$dir\n";
rmdir($dir);

$dir = tempdir('/tmp/foo', 'bar');
echo "$dir\n";
rmdir($dir);

// returns:
//   /tmp/BN4Wcd
//   /tmp/foo/baruLWFsN (if /tmp/foo exists, /tmp/baruLWFsN otherwise)

?>

This avoids the potential (although unlikely) race issue above and has the same behavior as the tempnam function.


I wanted to add a refinement to @Mario Mueller's answer, as his is subject to possible race conditions, however I believe the following should not be:

function tempdir(int $mode = 0700): string {
    do { $tmp = sys_get_temp_dir() . '/' . mt_rand(); }
    while (!@mkdir($tmp, $mode));
    return $tmp;
}

This works because mkdir returns false if $tmp already exists, causing the loop to repeat and try another name.

Note also that I've added handling for $mode, with a default that ensures the directory is accessible to the current user only, as mkdir's default is 0777 otherwise.

It is strongly advised that you use a shutdown function to ensure the directory is removed when no longer needed, even if your script exits by unexpected means*. To facilitate this, the full function that I use does this automatically unless the $auto_delete argument is set to false.

// Deletes a non-empty directory
function destroydir(string $dir): bool { 
    if (!is_dir($dir)) { return false; }

    $files = array_diff(scandir($dir), ['.', '..']);
    foreach ($files as $file) {
        if (is_dir("$dir/$file")) { destroydir("$dir/$file"); }
        else { unlink("$dir/$file"); }
    }
    return rmdir($dir); 
}

function tempdir(int $mode = 0700, bool $auto_delete = true): string {
    do { $tmp = sys_get_temp_dir() . '/' . mt_rand(); }
    while (!@mkdir($tmp, $mode));

    if ($auto_delete) {
        register_shutdown_function(function() use ($tmp) { destroydir($tmp); });
    }
    return $tmp;
}

This means that by default any temporary directory created by tempdir() will have permissions of 0700 and will be automatically deleted (along with its contents) when your script ends.

NOTE: *This may not be the case if the script is killed, for this you might need to look into registering a signal handler as well.