diff --git a/.changes/nextrelease/stream_wrapper_fixes.json b/.changes/nextrelease/stream_wrapper_fixes.json new file mode 100644 index 0000000000..8d79d0099f --- /dev/null +++ b/.changes/nextrelease/stream_wrapper_fixes.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "S3", + "description": "Updates the S3 stream wrapper to be able to write empty files for PHP 7+." + } +] \ No newline at end of file diff --git a/src/S3/StreamWrapper.php b/src/S3/StreamWrapper.php index 33954a186d..fbf6183b67 100644 --- a/src/S3/StreamWrapper.php +++ b/src/S3/StreamWrapper.php @@ -94,6 +94,9 @@ class StreamWrapper /** @var string The opened protocol (e.g., "s3") */ private $protocol = 's3'; + /** @var bool Keeps track of whether stream has been flushed since opening */ + private $isFlushed = false; + /** * Register the 's3://' stream wrapper * @@ -127,12 +130,16 @@ public static function register( public function stream_close() { + if ($this->body->getSize() === 0 && !($this->isFlushed)) { + $this->stream_flush(); + } $this->body = $this->cache = null; } public function stream_open($path, $mode, $options, &$opened_path) { $this->initProtocol($path); + $this->isFlushed = false; $this->params = $this->getBucketKey($path); $this->mode = rtrim($mode, 'bt'); @@ -156,6 +163,7 @@ public function stream_eof() public function stream_flush() { + $this->isFlushed = true; if ($this->mode == 'r') { return false; } diff --git a/tests/S3/StreamWrapperTest.php b/tests/S3/StreamWrapperTest.php index bd0ab9cac4..23f53eaf4b 100644 --- a/tests/S3/StreamWrapperTest.php +++ b/tests/S3/StreamWrapperTest.php @@ -170,6 +170,25 @@ public function testCanOpenWriteOnlyStreams() $this->assertEquals('test', (string) $cmd['Body']); } + public function testCanWriteEmptyFileToStream() + { + $history = new History(); + $this->client->getHandlerList()->appendSign(Middleware::history($history)); + $this->addMockResults($this->client, [new Result()]); + $s = fopen('s3://bucket/key', 'w'); + $this->assertEquals(0, fwrite($s, '')); + $this->assertTrue(fclose($s)); + + // Ensure that the stream was flushed even with zero characters, and + // that it only executed PutObject once. + $this->assertCount(1, $history); + $cmd = $history->getLastCommand(); + $this->assertEquals('PutObject', $cmd->getName()); + $this->assertEquals('bucket', $cmd['Bucket']); + $this->assertEquals('key', $cmd['Key']); + $this->assertEquals('', (string) $cmd['Body']); + } + /** * @expectedException \PHPUnit\Framework\Error\Warning * @expectedExceptionMessage 403 Forbidden