diff --git a/wire/core/PagefilesManager.php b/wire/core/PagefilesManager.php index 3add8171..2ea0c416 100644 --- a/wire/core/PagefilesManager.php +++ b/wire/core/PagefilesManager.php @@ -572,15 +572,23 @@ class PagefilesManager extends Wire { } /** - * Return a path where temporary files can be stored. + * Return a path where temporary files can be stored unique to this ProcessWire instance * * @return string * */ public function getTempPath() { static $wtd = null; - if(is_null($wtd)) $wtd = $this->wire(new WireTempDir($this->className() . $this->page->id)); + if(is_null($wtd)) { + $wtd = new WireTempDir(); + $this->wire($wtd); + $wtd->setMaxAge(3600); + $name = $wtd->createName('PFM'); + $wtd->create($name); + } return $wtd->get(); + // if(is_null($wtd)) $wtd = $this->wire(new WireTempDir($this->className() . $this->page->id)); + // return $wtd->get(); } - + } diff --git a/wire/core/WireTempDir.php b/wire/core/WireTempDir.php index 81730420..8030b3ad 100644 --- a/wire/core/WireTempDir.php +++ b/wire/core/WireTempDir.php @@ -3,51 +3,99 @@ /** * ProcessWire Temporary Directory Manager * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2017 by Ryan Cramer * https://processwire.com * */ class WireTempDir extends Wire { + + /** + * File automatically placed in created temp dirs for verification, contains timestamp + * + */ + const hiddenFileName = '.wtd'; protected $classRoot = null; // example: /site/assets/WireTempDir/ protected $tempDirRoot = null; // example: /site/assets/WireTempDir/.SomeName/ protected $tempDir = null; // example: /site/assets/WireTempDir/.SomeName/1/ protected $tempDirMaxAge = 120; // maximum age in seconds protected $autoRemove = true; // automatially remove temp dir when this class is destructed? + protected $createdName = ''; // name of runtime created random tempDirRoot, when applicable /** * Construct new temp dir * + * While this constructor accepts arguments, if you are in a multi-instance environment you should instead construct + * with no arguments, inject the ProcessWire instance, and then call the create() method with those arguments. + * * @param string|object $name Recommend providing the object that is using the temp dir, but can also be any string * @param string $basePath Base path where temp dirs should be created. Omit to use default (recommended). * @throws WireException if given a $root that doesn't exist * */ - public function __construct($name, $basePath = '') { - - if(is_object($name)) $name = wireClassName($name, false); - if(empty($name) || !is_string($name)) throw new WireException("A valid name (string) must be provided"); - + public function __construct($name = '', $basePath = '') { + if($name) $this->create($name, $basePath); + } + + /** + * Create the temporary directory + * + * This method should only be called once per instance of this class. If you specified a $name argument + * in the constructor, then you should not call this method because it will have already been called. + * + * @param string|object $name Recommend providing the object that is using the temp dir, but can also be any string + * @param string $basePath Base path where temp dirs should be created. Omit to use default (recommended). + * @throws WireException if given a $root that doesn't exist + * @return string Returns the root of the temporary directory. Use the get() method to get a dir for use. + * + */ + public function create($name = '', $basePath = '') { + + if(!is_null($this->tempDirRoot)) throw new WireException("Temp dir has already been created"); + if(empty($name)) $name = $this->createName(); + if(is_object($name)) $name = wireClassName($name, false); + if($basePath) { // they provide base path $basePath = rtrim($basePath, '/') . '/'; // ensure it ends with trailing slash - if(!is_dir($basePath)) throw new WireException("Provided base path doesn't exist: $basePath"); - if(!is_writable($basePath)) throw new WireException("Provided base path is not writiable: $basePath"); - + if(!is_dir($basePath)) throw new WireException("Provided base path doesn't exist: $basePath"); + if(!is_writable($basePath)) throw new WireException("Provided base path is not writiable: $basePath"); + } else { // we provide base path (root) $basePath = $this->wire('config')->paths->cache; - if(!is_dir($basePath)) $this->wire('files')->mkdir($basePath); + if(!is_dir($basePath)) $this->mkdir($basePath); } - + $basePath .= wireClassName($this, false) . '/'; - $this->classRoot = $basePath; - if(!is_dir($basePath)) $this->wire('files')->mkdir($basePath); - + $this->classRoot = $basePath; + if(!is_dir($basePath)) $this->mkdir($basePath); + $this->tempDirRoot = $basePath . ".$name/"; + + return $this->tempDirRoot; } + /** + * Create a randomized name for runtime temp dir + * + * @param string $prefix Optional prefix for name + * @return string + * + */ + public function createName($prefix = '') { + $pass = new Password(); + $this->wire($pass); + $len = mt_rand(10, 30); + $name = microtime() . '.' . $pass->randomBase64String($len, true); + $a = explode($name, '.'); + shuffle($a); + $name = $prefix . implode('O', $a); + $this->createdName = $name; + return $name; + } + /** * Set the max age of temp files (default=120 seconds) * @@ -79,41 +127,62 @@ class WireTempDir extends Wire { /** * Returns a temporary directory (path) * + * @param string $id Optional identifier to use (default=autogenerate) * @return string Returns path * @throws WireException If can't create temporary dir * */ - public function get() { + public function get($id = '') { + + static $level = 0; + + if(is_null($this->tempDirRoot)) throw new WireException("Please call the create() method before the get() method"); // first check if cached result from previous call if(!is_null($this->tempDir) && file_exists($this->tempDir)) return $this->tempDir; // find unique temp dir + $level++; $n = 0; do { + if($id) { + $tempDir = $this->tempDirRoot . $id . ($n ? "$n/" : "/"); + if(!$n) $id .= "-"; // i.e. id-1, for next iterations + } else { + $tempDir = $this->tempDirRoot . "$n/"; + } + if(!is_dir($tempDir)) break; $n++; - $tempDir = $this->tempDirRoot . "$n/"; - $exists = is_dir($tempDir); + /* if($exists) { // check if we can remove existing temp dir $time = filemtime($tempDir); if($time < time() - $this->tempDirMaxAge) { // dir is old and can be removed - if($this->wire('files')->rmdir($tempDir, true)) $exists = false; + if($this->rmdir($tempDir, true)) $exists = false; } } - } while($exists); + */ + } while(1); // create temp dir - if(!$this->wire('files')->mkdir($tempDir, true)) { + if(!$this->mkdir($tempDir, true)) { clearstatcache(); - if(!is_dir($tempDir) && !$this->wire('files')->mkdir($tempDir, true)) { - throw new WireException($this->_('Unable to create temp dir') . " - $tempDir"); + if(!is_dir($tempDir) && !$this->mkdir($tempDir, true)) { + if($level < 5) { + // try again, recursively + clearstatcache(); + $tempDir = $this->get($id . "L$level"); + } else { + $level--; + throw new WireException("Unable to create temp dir: $tempDir"); + } } } // cache result $this->tempDir = $tempDir; - + $level--; + return $tempDir; } @@ -126,8 +195,10 @@ class WireTempDir extends Wire { * */ public function remove() { + + static $classRuns = 0; - $errorMessage = $this->_('Unable to remove temp dir'); + $errorMessage = 'Unable to remove temp dir'; $success = true; if(is_null($this->tempDirRoot) || is_null($this->tempDir)) { @@ -137,49 +208,129 @@ class WireTempDir extends Wire { if(is_dir($this->tempDir)) { // remove temporary directory created by this instance - if(!wireRmdir($this->tempDir, true)) { - $this->error("$errorMessage - $this->tempDir"); + if(!$this->rmdir($this->tempDir, true)) { + $this->log("$errorMessage: $this->tempDir"); $success = false; } } if(is_dir($this->tempDirRoot)) { - // remove temporary directories created by other instances (like if one had failed at some point) - $numSubdirs = 0; - $pathname = ''; - foreach(new \DirectoryIterator($this->tempDirRoot) as $dir) { - if(!$dir->isDir() || $dir->isDot()) continue; - if($dir->getMTime() < (time() - $this->tempDirMaxAge)) { - // old dir found - $pathname = $dir->getPathname(); - if(!wireRmdir($pathname, true)) { - $this->error("$errorMessage - $pathname"); - $success = false; - } - } else { - $numSubdirs++; - } - } - if(!$numSubdirs) { - // if no subdirectories, we can remove the root - if(wireRmdir($this->tempDirRoot, true)) { - $success = true; - } else { - $this->error("$errorMessage - $pathname"); - $success = false; - } + if($this->createdName && strpos($this->tempDirRoot, "/.$this->createdName")) { + // if tempDirRoot is just for this PW instance, we can remove it now + $this->rmdir($this->tempDirRoot, true); + } else { + // if it is potentially used by multiple instances, then remove only expired files + $this->removeExpiredDirs($this->tempDirRoot); } } + if(!$classRuns && is_dir($this->classRoot)) { + $this->removeExpiredDirs($this->classRoot, 86400); + } + + $classRuns++; + return $success; } + /** + * Remove expired directories in the given $path + * + * Also removes $path if it's found that everything in it is expired. + * + * @param string $path + * @param int|null Optionally specify a max age to override default setting. + * @return bool + * + */ + protected function removeExpiredDirs($path, $maxAge = null) { + + if(!is_dir($path)) return false; + if(!is_int($maxAge)) $maxAge = $this->tempDirMaxAge; + + $numSubdirs = 0; + $pathname = ''; + $oldestAllowedFileTime = time() - $maxAge; + $success = true; + + foreach(new \DirectoryIterator($path) as $dir) { + + if(!$dir->isDir() || $dir->isDot()) continue; + + // if the directory itself is not expired, then nothing in it is either + if($dir->getMTime() >= $oldestAllowedFileTime) { + $numSubdirs++; + continue; + } + + // old dir found: check times on files/dirs within that dir + $pathname = rtrim($dir->getPathname(), "/\\") . DIRECTORY_SEPARATOR; + + // if our .wtd identifier file isn't present, ignore the directory + if(!is_file($pathname . self::hiddenFileName)) continue; + + $removeDir = true; + $newestFileTime = $this->getNewestModTime($pathname); + if($newestFileTime >= $oldestAllowedFileTime) $removeDir = false; + + if($removeDir) { + if(!$this->rmdir($pathname, true)) { + $this->log("Unable to remove: $path"); + $success = false; + } + } else { + $numSubdirs++; + } + } + + if(!$numSubdirs) { + // if no subdirectories, we can remove the root + if($this->rmdir($path, true)) { + $success = true; + } else { + $this->log("Unable to remove: $pathname"); + $success = false; + } + } + + return $success; + } + + /** + * Get the newest modification time of a file in $path, recursively + * + * @param string $path Path to start from + * @param int $maxDepth + * @return int + * + */ + protected function getNewestModTime($path, $maxDepth = 5) { + static $level = 0; + $level++; + // check if any files in the directory are newer than maxAge + $newest = filemtime($path); + foreach(new \DirectoryIterator($path) as $file) { + if($file->isDot()) continue; + $mtime = $file->getMTime(); + if($mtime > $newest) $newest = $mtime; + if($level < $maxDepth && $file->isDir()) { + $mtime = $this->getNewestModTime($file->getPathname(), $maxDepth); + if($mtime > $newest) $newest = $mtime; + } + } + $level--; + return $newest; + } + /** * Clear all temporary directories created by this class * */ public function removeAll() { - if($this->classRoot && is_dir($this->classRoot)) return wireRmdir($this->classRoot, true); + if($this->classRoot && is_dir($this->classRoot)) { + // note: use of $files->rmdir rather than $this->rmdir is intentional + return $this->wire('files')->rmdir($this->classRoot, true); + } return false; } @@ -192,4 +343,36 @@ class WireTempDir extends Wire { public function __toString() { return $this->get(); } + + /** + * Create a temporary directory + * + * @param string $dir + * @param bool $recursive + * @return bool + * + */ + protected function mkdir($dir, $recursive = false) { + if($this->wire('files')->mkdir($dir, $recursive)) { + $dir = rtrim($dir, "/\\") . DIRECTORY_SEPARATOR; + file_put_contents($dir . self::hiddenFileName, time()); + } else { + return false; + } + } + + /** + * Remove a temporary directory + * + * @param string $dir + * @param bool $recursive + * @return bool + * + */ + protected function rmdir($dir, $recursive = false) { + $dir = rtrim($dir, "/\\") . DIRECTORY_SEPARATOR; + if(!is_file($dir . self::hiddenFileName)) return false; + unlink($dir . self::hiddenFileName); + return $this->wire('files')->rmdir($dir, $recursive); + } }