[Filesystem] added tempnam() stream wrapper aware version of PHP's native tempnam() and fixed dumpFile to allow dumping to streams
This commit is contained in:
parent
0bb46c1ae2
commit
c6a774761d
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
2.8.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added tempnam() a stream aware version of PHP's native tempnam()
|
||||||
|
|
||||||
2.6.0
|
2.6.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -452,6 +452,53 @@ class Filesystem
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary file with support for custom stream wrappers.
|
||||||
|
*
|
||||||
|
* @param string $dir The directory where the temporary filename will be created.
|
||||||
|
* @param string $prefix The prefix of the generated temporary filename.
|
||||||
|
* Note: Windows uses only the first three characters of prefix.
|
||||||
|
*
|
||||||
|
* @return string The new temporary filename (with path), or false on failure.
|
||||||
|
*/
|
||||||
|
public function tempnam($dir, $prefix)
|
||||||
|
{
|
||||||
|
$limit = 10;
|
||||||
|
list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir);
|
||||||
|
|
||||||
|
// If no scheme or scheme is "file" create temp file in local filesystem
|
||||||
|
if (null === $scheme || 'file' === $scheme) {
|
||||||
|
$tmpFile = tempnam($hierarchy, $prefix);
|
||||||
|
|
||||||
|
// If tempnam failed or no scheme return the filename otherwise prepend the scheme
|
||||||
|
return false === $tmpFile || null === $scheme ? $tmpFile : $scheme.'://'.$tmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop until we create a valid temp file or have reached $limit attempts
|
||||||
|
for ($i = 0; $i < $limit; $i++) {
|
||||||
|
|
||||||
|
// Create a unique filename
|
||||||
|
$tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true);
|
||||||
|
|
||||||
|
// Use fopen instead of file_exists as some streams do not support stat
|
||||||
|
// Use mode 'x' to atomically check existence and create to avoid a TOCTOU vulnerability
|
||||||
|
$handle = @fopen($tmpFile, 'x');
|
||||||
|
|
||||||
|
// If unsuccessful restart the loop
|
||||||
|
if (false === $handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file if it was successfully opened
|
||||||
|
@fclose($handle);
|
||||||
|
|
||||||
|
return $tmpFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomically dumps content into a file.
|
* Atomically dumps content into a file.
|
||||||
*
|
*
|
||||||
@ -472,7 +519,7 @@ class Filesystem
|
|||||||
throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
|
throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpFile = tempnam($dir, basename($filename));
|
$tmpFile = $this->tempnam($dir, basename($filename));
|
||||||
|
|
||||||
if (false === @file_put_contents($tmpFile, $content)) {
|
if (false === @file_put_contents($tmpFile, $content)) {
|
||||||
throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
|
throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
|
||||||
@ -501,4 +548,19 @@ class Filesystem
|
|||||||
|
|
||||||
return $files;
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)).
|
||||||
|
*
|
||||||
|
* @param string $filename The filename to be parsed.
|
||||||
|
*
|
||||||
|
* @return array The filename scheme and hierarchical part
|
||||||
|
*/
|
||||||
|
private function getSchemeAndHierarchy($filename)
|
||||||
|
{
|
||||||
|
$components = explode('://', $filename, 2);
|
||||||
|
|
||||||
|
return count($components) >= 2 ? array($components[0], $components[1]) : array(null, $components[0]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,11 +30,15 @@ $filesystem->rename($origin, $target);
|
|||||||
|
|
||||||
$filesystem->symlink($originDir, $targetDir, $copyOnWindows = false);
|
$filesystem->symlink($originDir, $targetDir, $copyOnWindows = false);
|
||||||
|
|
||||||
|
$filesystem->tempnam($dir, $prefix);
|
||||||
|
|
||||||
$filesystem->makePathRelative($endPath, $startPath);
|
$filesystem->makePathRelative($endPath, $startPath);
|
||||||
|
|
||||||
$filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array());
|
$filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array());
|
||||||
|
|
||||||
$filesystem->isAbsolutePath($file);
|
$filesystem->isAbsolutePath($file);
|
||||||
|
|
||||||
|
$filesystem->dumpFile($file, $content);
|
||||||
```
|
```
|
||||||
|
|
||||||
Resources
|
Resources
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Filesystem\Tests;
|
namespace Symfony\Component\Filesystem\Tests;
|
||||||
|
|
||||||
|
use Phar;
|
||||||
/**
|
/**
|
||||||
* Test class for Filesystem.
|
* Test class for Filesystem.
|
||||||
*/
|
*/
|
||||||
@ -946,6 +947,116 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTempnam()
|
||||||
|
{
|
||||||
|
$dirname = $this->workspace;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'foo');
|
||||||
|
|
||||||
|
$this->assertNotFalse($filename);
|
||||||
|
$this->assertFileExists($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithFileScheme()
|
||||||
|
{
|
||||||
|
$scheme = 'file://';
|
||||||
|
$dirname = $scheme.$this->workspace;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'foo');
|
||||||
|
|
||||||
|
$this->assertNotFalse($filename);
|
||||||
|
$this->assertStringStartsWith($scheme, $filename);
|
||||||
|
$this->assertFileExists($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithMockScheme()
|
||||||
|
{
|
||||||
|
// We avoid autoloading via ClassLoader as stream_wrapper_register creates the object
|
||||||
|
if (!@include __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MockStream'.DIRECTORY_SEPARATOR.'MockStream.php') {
|
||||||
|
$this->markTestSkipped('Unable to load mock:// stream.');
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_wrapper_register('mock', 'MockStream\MockStream');
|
||||||
|
|
||||||
|
$scheme = 'mock://';
|
||||||
|
$dirname = $scheme.$this->workspace;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'foo');
|
||||||
|
|
||||||
|
$this->assertNotFalse($filename);
|
||||||
|
$this->assertStringStartsWith($scheme, $filename);
|
||||||
|
$this->assertFileExists($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithZlibSchemeFails()
|
||||||
|
{
|
||||||
|
$scheme = 'compress.zlib://';
|
||||||
|
$dirname = $scheme.$this->workspace;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'bar');
|
||||||
|
|
||||||
|
// The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false
|
||||||
|
$this->assertFalse($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithPHPTempSchemeFails()
|
||||||
|
{
|
||||||
|
$scheme = 'php://temp';
|
||||||
|
$dirname = $scheme;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'bar');
|
||||||
|
|
||||||
|
$this->assertNotFalse($filename);
|
||||||
|
$this->assertStringStartsWith($scheme, $filename);
|
||||||
|
|
||||||
|
// The php://temp stream deletes the file after close
|
||||||
|
$this->assertFileNotExists($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithPharSchemeFails()
|
||||||
|
{
|
||||||
|
// Skip test if Phar disabled phar.readonly must be 0 in php.ini
|
||||||
|
if (!Phar::canWrite()) {
|
||||||
|
$this->markTestSkipped('This test cannot run when phar.readonly is 1.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = 'phar://';
|
||||||
|
$dirname = $scheme.$this->workspace;
|
||||||
|
$pharname = 'foo.phar';
|
||||||
|
|
||||||
|
$p = new Phar($this->workspace.'/'.$pharname, 0, $pharname);
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, $pharname.'/bar');
|
||||||
|
|
||||||
|
// The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false
|
||||||
|
$this->assertFalse($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamWithHTTPSchemeFails()
|
||||||
|
{
|
||||||
|
$scheme = 'http://';
|
||||||
|
$dirname = $scheme.$this->workspace;
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'bar');
|
||||||
|
|
||||||
|
// The http:// scheme is read-only
|
||||||
|
$this->assertFalse($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTempnamOnUnwritableFallsBackToSysTmp()
|
||||||
|
{
|
||||||
|
$scheme = 'file://';
|
||||||
|
$dirname = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'does_not_exist';
|
||||||
|
|
||||||
|
$filename = $this->filesystem->tempnam($dirname, 'bar');
|
||||||
|
|
||||||
|
$this->assertNotFalse($filename);
|
||||||
|
$this->assertStringStartsWith(rtrim($scheme.sys_get_temp_dir(), DIRECTORY_SEPARATOR), $filename);
|
||||||
|
$this->assertFileExists($filename);
|
||||||
|
|
||||||
|
// Tear down
|
||||||
|
unlink($filename);
|
||||||
|
}
|
||||||
|
|
||||||
public function testDumpFile()
|
public function testDumpFile()
|
||||||
{
|
{
|
||||||
$filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
|
$filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
|
||||||
@ -1000,6 +1111,29 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->assertSame('bar', file_get_contents($filename));
|
$this->assertSame('bar', file_get_contents($filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDumpFileWithFileScheme()
|
||||||
|
{
|
||||||
|
$scheme = 'file://';
|
||||||
|
$filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
|
||||||
|
|
||||||
|
$this->filesystem->dumpFile($filename, 'bar', null);
|
||||||
|
|
||||||
|
$this->assertFileExists($filename);
|
||||||
|
$this->assertSame('bar', file_get_contents($filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDumpFileWithZlibScheme()
|
||||||
|
{
|
||||||
|
$scheme = 'compress.zlib://';
|
||||||
|
$filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
|
||||||
|
|
||||||
|
$this->filesystem->dumpFile($filename, 'bar', null);
|
||||||
|
|
||||||
|
// Zlib stat uses file:// wrapper so remove scheme
|
||||||
|
$this->assertFileExists(str_replace($scheme, '', $filename));
|
||||||
|
$this->assertSame('bar', file_get_contents($filename));
|
||||||
|
}
|
||||||
|
|
||||||
public function testCopyShouldKeepExecutionPermission()
|
public function testCopyShouldKeepExecutionPermission()
|
||||||
{
|
{
|
||||||
$this->markAsSkippedIfChmodIsMissing();
|
$this->markAsSkippedIfChmodIsMissing();
|
||||||
|
@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*
|
||||||
|
* This class is based on VariableStream from the PHP Manual, which is licenced
|
||||||
|
* under Creative Commons Attribution 3.0 Licence copyright (c) the PHP
|
||||||
|
* Documentation Group
|
||||||
|
*
|
||||||
|
* @url http://php.net/manual/en/stream.streamwrapper.example-1.php
|
||||||
|
* @url http://php.net/license/
|
||||||
|
* @url http://creativecommons.org/licenses/by/3.0/legalcode
|
||||||
|
*/
|
||||||
|
namespace MockStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock stream class to be used with stream_wrapper_register.
|
||||||
|
*
|
||||||
|
* stream_wrapper_register('mock', 'MockStream\MockStream')
|
||||||
|
*/
|
||||||
|
class MockStream {
|
||||||
|
private $str_overloaded;
|
||||||
|
private $content;
|
||||||
|
private $position;
|
||||||
|
private $atime;
|
||||||
|
private $mtime;
|
||||||
|
private $ctime;
|
||||||
|
private $path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens file or URL.
|
||||||
|
*
|
||||||
|
* @param string $path Specifies the URL that was passed to the original function
|
||||||
|
* @param string $mode The mode used to open the file, as detailed for fopen()
|
||||||
|
* @param int $options Holds additional flags set by the streams API
|
||||||
|
* @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path should be set to the full path of the file/resource that was actually opened
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path) {
|
||||||
|
// Is mbstring.func_overload applied to string functions (bit 2 set)
|
||||||
|
$this->str_overloaded = (bool) (ini_get('mbstring.func_overload') & (1 << 2));
|
||||||
|
$this->path = $path;
|
||||||
|
$this->content = '';
|
||||||
|
$this->position = 0;
|
||||||
|
$this->atime = 0;
|
||||||
|
$this->mtime = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from stream.
|
||||||
|
*
|
||||||
|
* @param int $count How many bytes of data from the current position should be returned
|
||||||
|
*
|
||||||
|
* @return string The data
|
||||||
|
*/
|
||||||
|
public function stream_read($count) {
|
||||||
|
$ret = $this->substr($this->varname, $this->position, $count);
|
||||||
|
$this->position += $this->strlen($ret);
|
||||||
|
$this->atime = time();
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to stream.
|
||||||
|
*
|
||||||
|
* @param string $data Data to write to the stream
|
||||||
|
*
|
||||||
|
* @return int Number of bytes that were successfully stored, or 0 if none could be stored
|
||||||
|
*/
|
||||||
|
public function stream_write($data) {
|
||||||
|
$left = $this->substr($this->content, 0, $this->position);
|
||||||
|
$right = $this->substr($this->content, $this->position + $this->strlen($data));
|
||||||
|
$this->content = $left.$data.$right;
|
||||||
|
$this->position += $this->strlen($data);
|
||||||
|
$this->mtime = time();
|
||||||
|
$this->ctime = time();
|
||||||
|
|
||||||
|
return $this->strlen($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the current position of a stream.
|
||||||
|
*
|
||||||
|
* @return int The current position of the stream
|
||||||
|
*/
|
||||||
|
public function stream_tell() {
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for end-of-file on a file pointer.
|
||||||
|
*
|
||||||
|
* @return bool Return true if the read/write position is at the end of the stream and if no more data is available to be read, or false otherwise
|
||||||
|
*/
|
||||||
|
public function stream_eof() {
|
||||||
|
return $this->position >= $this->strlen($this->content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeks to specific location in a stream.
|
||||||
|
*
|
||||||
|
* @param string $offset The stream offset to seek to
|
||||||
|
* @param int $whence Set position based on value
|
||||||
|
*
|
||||||
|
* @return bool Return true if the position was updated, false otherwise
|
||||||
|
*/
|
||||||
|
public function stream_seek($offset, $whence) {
|
||||||
|
switch ($whence) {
|
||||||
|
case SEEK_SET:
|
||||||
|
if ($offset < $this->strlen($this->content) && 0 <= $offset) {
|
||||||
|
$this->position = $offset;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEEK_CUR:
|
||||||
|
if (0 <= $offset) {
|
||||||
|
$this->position += $offset;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEEK_END:
|
||||||
|
if (0 <= $this->strlen($this->content) + $offset) {
|
||||||
|
$this->position = $this->strlen($this->content) + $offset;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change stream options, only touch is supported.
|
||||||
|
*
|
||||||
|
* @param string $path The file path or URL to set metadata
|
||||||
|
* @param array $option
|
||||||
|
* @param array $value Additional arguments for the option
|
||||||
|
*
|
||||||
|
* @return bool Return true on success or fale on failure or if option is not implemented
|
||||||
|
*/
|
||||||
|
public function stream_metadata($path, $option, $value) {
|
||||||
|
if (STREAM_META_TOUCH === $option) {
|
||||||
|
$now = array_key_exists(0, $value) ? $value[0] : time();
|
||||||
|
$this->atime = array_key_exists(1, $value) ? $value[1] : $now;
|
||||||
|
$this->mtime = $now;
|
||||||
|
$this->ctime = $now;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve information about a stream.
|
||||||
|
*
|
||||||
|
* @return array Stream stats
|
||||||
|
*/
|
||||||
|
public function stream_stat() {
|
||||||
|
return array(
|
||||||
|
'dev' => 0,
|
||||||
|
'ino' => 0,
|
||||||
|
'mode' => 33188, // 100644
|
||||||
|
'nlink' => 1,
|
||||||
|
'uid' => 0,
|
||||||
|
'gid' => 0,
|
||||||
|
'rdev' => 0,
|
||||||
|
'size' => $this->strlen($this->content),
|
||||||
|
'atime' => $this->atime,
|
||||||
|
'mtime' => $this->mtime,
|
||||||
|
'ctime' => $this->ctime,
|
||||||
|
'blksize' => 4096,
|
||||||
|
'blocks' => 8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve information about a url, added as called by PHP's builtin functions.
|
||||||
|
*
|
||||||
|
* @param string $path The file path or URL to stat
|
||||||
|
* @param array $flags Holds additional flags set by the streams API
|
||||||
|
*
|
||||||
|
* @return array File stats
|
||||||
|
*/
|
||||||
|
public function url_stat($path, $flags) {
|
||||||
|
return $this->stream_stat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of bytes of the given string even when strlen is overloaded to mb_strlen.
|
||||||
|
*
|
||||||
|
* @param string $string The string being measured for bytes
|
||||||
|
*
|
||||||
|
* @return int The number of bytes in the string on success, and 0 if the string is empty
|
||||||
|
*/
|
||||||
|
protected function strlen($string) {
|
||||||
|
return function_exists('mb_strlen') && $this->str_overloaded ? mb_strlen($string, '8bit') : strlen($string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the portion of string specified by the start and length parameters even when substr is overloaded to mb_substr.
|
||||||
|
*
|
||||||
|
* @param string $string The input string which must be one character or longer
|
||||||
|
* @param start $int Starting position in bytes
|
||||||
|
* @param length $int Length in bytes which if omitted or NULL is passed, extracts all bytes to the end of the string
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function substr($string, $start, $length = null) {
|
||||||
|
return function_exists('mb_substr') && $this->str_overloaded ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user