Closed
Description
Description
The following script:
- Registers a simple stream for the
up
protocol which doesfwrite
under the hood. - Runs with 256 MB of memory available.
- Performs a
copy
operation of a source file into a destination file using the defined stream.
<?php
class UP {
/**
* Default protocol
*/
const DEFAULT_PROTOCOL = 'up';
const ALLOWED_MODES = [ 'r', 'w', 'a', 'x' ];
public $context;
protected $mode;
protected $file;
protected $path;
protected $uri;
protected $seekable;
private $protocol;
private $localpath;
/**
* constructor.
*
*/
public function __construct() {
$this->protocol = self::DEFAULT_PROTOCOL;
}
/**
* Register the Stream.
*
* Will unregister stream first if it's already registered
*
* @since 1.0.0
* @access public
*
* @return bool true if success, false if failure
*/
public function register() {
$this->localpath = getcwd();
return stream_wrapper_register( $this->protocol, get_called_class(), STREAM_IS_URL );
}
public function stream_open( $path, $mode, $options, &$opened_path ) {
$path = sprintf( "%s%s", $this->localpath, $this->trim_path( $path ) );
$mode = rtrim( $mode, 'bt+' );
$file = fopen( $path, $mode );
$this->file = $file;
$this->path = $path;
$this->mode = $mode;
return true;
}
public function stream_close() {
if ( ! $this->file ) {
return true;
}
fclose( $this->file );
$this->file = null;
$this->path = null;
$this->mode = null;
return true;
}
public function stream_eof() {
return feof( $this->file );
}
public function stream_read( $count ) {
$string = fread( $this->file, $count );
if ( false === $string ) {
return '';
}
return $string;
}
public function stream_flush() {
return fflush( $this->file );
}
public function stream_seek( $offset, $whence ) {
if ( ! $this->seekable ) {
return false;
}
$result = fseek( $this->file, $offset, $whence );
return true;
}
public function stream_write( $data ) {
if ( 'r' === $this->mode ) {
return false;
}
$length = fwrite( $this->file, $data );
if ( false === $length ) {
return false;
}
return $length;
}
public function unlink( $path ) {
return true;
}
public function stream_stat() {
return fstat( $this->file );
}
public function url_stat( $path, $flags ) {
$path = $this->trim_path( $path );
$path = sprintf( "%s%s", $this->localpath, $this->trim_path( $path ) );
$file = @fopen( $path, "r" );
if ( ! $file ) {
return false;
}
return fstat( $file );
fclose( $file );
}
public function stream_tell() {
return $this->file ? ftell( $this->file ) : false;
}
public function rename( $path_from, $path_to ) {
return false;
}
public function mkdir( $path, $mode, $options ) {
$path = $this->trim_path( $path );
$path = sprintf( "%s%s", $this->localpath, $this->trim_path( $path ) );
return mkdir( $path, 0777, true );
}
public function stream_metadata( $path, $option, $value ) {
$path = sprintf( "%s%s", $this->localpath, $this->trim_path( $path ) );
switch ( $option ) {
case STREAM_META_TOUCH:
if ( false === file_exists( $path ) ) {
$file = fopen( $path, 'w' );
if ( is_resource( $file ) ) {
$result = fflush( $file );
return fclose( $file ) && $result;
}
return false;
}
return true;
default:
return false;
}
}
public function stream_cast( $cast_as ) {
if ( ! is_null( $this->file ) ) {
return $this->file;
}
return false;
}
protected function string_to_resource( $data, $mode ) {
// Create a temporary file
$tmp_handler = tmpfile();
switch ( $mode ) {
case 'a':
// Make sure pointer is at end of file for appends
fseek( $tmp_handler, 0, SEEK_END );
break;
default:
// Need to rewind file pointer as fwrite moves it to EOF
rewind( $tmp_handler );
}
return $tmp_handler;
}
protected function trim_path( $path ) {
return ltrim( $path, 'up:/\\' );
}
}
ini_set( 'memory_limit', 256 * 1024 * 102 );
$up = new UP();
$up->register();
copy( $argv[1], sprintf( "up://%s", $argv[2] ) );
<?php
Ideally, this stream wrapper should be able to copy any file, but if the file is large enough it dies because memory gets exhausted.
Example to reproduce
dd if=/dev/zero of=big bs=1M count=300
php up.php big big-copy
Resulted in this output:
Fatal error: Allowed memory size of 26738688 bytes exhausted (tried to allocate 314572832 bytes) in /Users/alessandro/Ongoing/php-uploads/up.php on line 199
While no output and a successful copy was expected.
This seems to happen after 5cbe5a5, where chunking was disabled for streams.
copy
passes a buffer to _php_stream_write_buffer
that is as large as PHP_STREAM_MMAP_MAX
, defined at 512MB. This means that files smaller than 512MB require at least their size in terms of memory to be copied, and files larger than 512MB requires at least 512MB of memory available.
This has been tested with PHP 8.0, 8.1, and now lastly with
PHP 8.3.1 (cli) (built: Dec 20 2023 12:44:38) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.1, Copyright (c) Zend Technologies
with Zend OPcache v8.3.1, Copyright (c), by Zend Technologies
PHP Version
PHP 8.3.1
Operating System
No response