Making a temporary dir for unpacking a zipfile into
Asked Answered
F

8

33

I have a script that checks a zipfile containing a number of matching PDF+textfiles. I want to unpack, or somehow read the textfiles from the zipfile, and just pick out some information from the textfile to see that the file version is correct.

I was looking at the tempnam() function to find an equivalent to make a tempdir, but maybe someone has a better solution for the problem.

The indexfile looks something like this. (-> is for TAB char). I have made the function to extract the version from the textfile and to check if its correct already, its only the unpacking, tmpdir or some other solution im looking for.

1000->filename->file version->program version->customer no->company no->distribution
2000->pagenumber->more info->more info->...
Freighter answered 10/11, 2009 at 13:1 Comment(0)
T
37

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.

Treacle answered 10/11, 2009 at 13:10 Comment(10)
This implementation is subject to race condition. Between unlink and mkdir. Highly unlikely it'll give you any issues, but something to note if it does.Burdened
why tempdir function accepts $dir and $prefix but never are used??Paleoclimatology
That's beautiful! I think the race-condition goes away (I didn't verify) if you move the unlink further down in the code, and add something, say an underscore, to the end or beginning of the new directory to be created.Manducate
But this does not have the special property of tmpfile(), which will automagically wipe itself when all handles to it are closed... then again, directories don't have handles.Apportionment
@MilindR I used register_shutdown_function(function() use ($tmpfile) { `rm -rf "$tmpfile"`; });Bartels
Nice @MaxTsepkov! With a bit of luck, one can find an exploit to set $tmpfile to "/" and remove quite some files...Determinism
@PatrickAllaert if a malicious user has write access to code he already can execute rm -rf / with php privilege. It can be exploited if you still running register_globals on though in this case you most probably have outdated code and hence lot more issues.Bartels
What do you mean by "in a linux system a file and a directory are theoretically the same thing" ?Edla
@Paleoclimatology Since between choosing a unique name and creating an object with such name there can be a time where someone else create the object with your very same name, you are requested to give a $prefix that minimize the chances for a name clash. Usually give your application name as $prefix is a good thingEdla
Seems messy to me to create a file and delete it immediately just to generate a random string. Why not use something you can guarantee is unique, such as some combination of username, timestamp, and a random string? If you're still worried, you could do a do-while to make sure it doesn't exist before you create it.Haga
F
22

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)
Fizzy answered 3/5, 2015 at 6:29 Comment(4)
Care to comment on the downvote? I originally landed on this question attempting to solve the problem cleanly, and couldn't find an answer I was satisfied with using in production, so I wanted to share what I wrote for others that end up here looking for the same thing. SO etiquette prefers that you leave a comment or suggest an edit or improvement when downvoting something.Fizzy
Nice. Should Attenot be Attempt?Mite
I like your solution but if the code in the loop fails too many times it'll just return the most-recently-attempted path rather than indicating that it failed to find a valid path, which could make code that uses this function fail.Ramakrishna
Rather than using '/', perhaps this should utilize DIRECTORY_SEPARATOR?Bruyn
C
12

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.

Chandrachandragupta answered 24/6, 2013 at 16:25 Comment(2)
I think this could blow up if $dir contained spaces. It would be safer to do $tmpdir = "--tmpdir=" . addslashes($dir);Jonjona
escapeshellarg would be more appropriate I think.Glomerulus
L
7

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.

Limpid answered 9/8, 2017 at 8:21 Comment(2)
Great answer. Just one detail: I needed to add unlink($tmp) inside do {} while(). If not, I get an infinite loop since $tmp is created as a regular file and therefore mkdir always return false.Courland
WARNING: never use code above as is. It will cause web server (Apache for me) create HUGE junk in /tmp folder. It works like a bomb, eating all i-nodes on partition in which /tmp is located. Server reboot and long time for removing millions of empty files will be required! Fix is simple add unlink($tmp) as in comment above.Ingenue
E
1

The "mkdir" function raises a warning if the directory already exists, so you can catch this using "@mkdir" and avoid any race condition:

function tempDir($parent = null)
{
    // Prechecks
    if ($parent === null) {
        $parent = sys_get_temp_dir();
    }
    $parent = rtrim($parent, '/');
    if (!is_dir($parent) || !is_writeable($parent)) {
        throw new Exception(sprintf('Parent directory is not writable: %s', $parent));
    }

    // Create directory
    do  { 
        $directory = $parent . '/' . mt_rand();
        $success = @mkdir($directory);
    }
    while (!$success);

    return $directory; 
}
Elaterium answered 21/12, 2015 at 5:39 Comment(2)
OK, this could probably do with a guard in the while loop in case something crazy happens ($parent points to a file, for example), but the general mechanism looks sound - isn't this more robust than the two higher-rated answers?Squamosal
I edited my answer to also check if the parent is a directory. So now it can go crazy only if disk is full. And yes I think it's better than the above because it's race safe.Elaterium
M
1

There are a lot of overkill answers to this question. One simple answer would be:

$tempdir = tempnam(sys_get_temp_dir()) . 'dir';
mkdir($tempdir);
  1. Obtain a temporary file name.
  2. Create the directory (append a suffix to temp file, to avoid file name collision.)
  3. Done.
Mcdowell answered 10/7, 2016 at 12:39 Comment(3)
Your approach has the side-effect of creating an unused temporary file and not removing it.Jonjona
For all of the effort in hours to get things to clean up correctly, you're almost better using this solution, and not cleaning it up. Instead have a cron or scheduled task go back and remove any directory that's over a day old, at the end of the day or something. I agree everything here is overkill, and I see it a lot. There will always be something the clip doesn't do, and either way, the OP didn't ask about cleanup they just wanted to know how to create a directory. Just noticed how old this is, oh well :)Gladsome
In case of cleanup, this is safe if you remove them in reverse order, removing temporary directory first and then the placeholder file ($tempdir without dir suffix). That way there is no race condition risk in the clean-up phase.Meara
C
0

Another possibility is to use the temporal file as a kind of semaphore to guarantee the unicity of the directory name. Then, create a directory whose name is based on the file name.

define ('TMP_DIR', '/tmp'); // sys_get_temp_dir() PHP 5 >= 5.2.1
define ('TMP_DIR_PREFIX', 'tmpdir_');
define ('TMP_DIR_SUFFIX', '.d');

/* ************************************************************************** */

function createTmpDir() {
  $tmpFile = tempnam(TMP_DIR, TMP_DIR_PREFIX);
  $tmpDir = $tmpFile.TMP_DIR_SUFFIX;
  mkdir($tmpDir);
  return $tmpDir;
}

function rmTmpDir($tmpDir) {
  $offsetSuffix = -1 * strlen(TMP_DIR_SUFFIX);
  assert(strcmp(substr($tmpDir, $offsetSuffix), TMP_DIR_SUFFIX) === 0);
  $tmpFile = substr($tmpDir, 0, $offsetSuffix);

  // Removes non-empty directory
  $command = "rm -rf $tmpDir/";
  exec($command);
  // rmdir($tmpDir);

  unlink($tmpFile);
}

/* ************************************************************************** */
Clarinda answered 4/11, 2014 at 12:19 Comment(0)
D
0

Figure I will proved the easy answer. Just Set Prefix to your application specific string

$tmpDir = sprintf('%s%sPREFIX-%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, mt_rand());

mkdir($tmpDir);
Daren answered 4/7, 2021 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.