diff --git a/Storage/src/StreamWrapper.php b/Storage/src/StreamWrapper.php index 630ae47c5044..9b9304f05d83 100644 --- a/Storage/src/StreamWrapper.php +++ b/Storage/src/StreamWrapper.php @@ -23,7 +23,7 @@ /** * A streamWrapper implementation for handling `gs://bucket/path/to/file.jpg`. - * Note that you can only open a file with mode 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', or 'at'. + * Note that you can only open a file with mode 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at', 'x', 'xb', or 'xt'. * * See: http://php.net/manual/en/class.streamwrapper.php */ @@ -211,7 +211,8 @@ public static function getClient($protocol = null) * download the file to see if it can be opened. * * @param string $path The path of the resource to open - * @param string $mode The fopen mode. Currently supports ('r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at') + * @param string $mode The fopen mode. Currently supports ('r', 'rb', 'rt', + * 'w', 'wb', 'wt', 'a', 'ab', 'at', 'x', 'xb', 'xt') * @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK * @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set * @return bool @@ -264,6 +265,22 @@ public function stream_open($path, $mode, $flags, &$openedPath) $options + ['name' => $name] ) ); + } elseif ($mode == 'x') { + try { + if ($this->bucket->object($this->file)->exists()) { + return $this->returnError('File already exists.', $flags); + } + } catch (ServiceException $ex) { + return $this->returnError($ex->getMessage(), $flags); + } + + $this->stream = new WriteStream(null, $options); + $this->stream->setUploader( + $this->bucket->getStreamableUploader( + $this->stream, + $options + ['name' => $this->file, 'ifGenerationMatch' => 0] + ) + ); } elseif ($mode == 'r') { try { // Lazy read from the source diff --git a/Storage/tests/System/StreamWrapper/WriteTest.php b/Storage/tests/System/StreamWrapper/WriteTest.php index a7f0d93f80ae..9c4d47602db0 100644 --- a/Storage/tests/System/StreamWrapper/WriteTest.php +++ b/Storage/tests/System/StreamWrapper/WriteTest.php @@ -80,4 +80,30 @@ public function testStreamingWrite() $this->assertFileExists($this->fileUrl); } + + public function testFwriteWithXMode() + { + $this->assertFileDoesNotExist($this->fileUrl); + + $output = 'This is a test with x mode'; + $fd = fopen($this->fileUrl, 'x'); + $this->assertIsResource($fd); + $this->assertEquals(strlen($output), fwrite($fd, $output)); + $this->assertTrue(fclose($fd)); + + $this->assertFileExists($this->fileUrl); + } + + public function testFwriteWithXModeFailsIfExists() + { + $this->assertFileDoesNotExist($this->fileUrl); + + // Create the file first + touch($this->fileUrl); + $this->assertFileExists($this->fileUrl); + + // Try to open with 'x' mode, should fail + $fd = @fopen($this->fileUrl, 'x'); + $this->assertFalse($fd); + } } diff --git a/Storage/tests/Unit/StreamWrapperTest.php b/Storage/tests/Unit/StreamWrapperTest.php index 03c24009d809..5b462afc4898 100644 --- a/Storage/tests/Unit/StreamWrapperTest.php +++ b/Storage/tests/Unit/StreamWrapperTest.php @@ -83,10 +83,67 @@ public function testOpeningNonExistentFileReturnsFalse() */ public function testUnknownOpenMode() { + $fp = @fopen('gs://my_bucket/existing_file.txt', 'z'); + $this->assertFalse($fp); + } + + /** + * @group storageWrite + */ + public function testOpeningExistingFileWithXModeReturnsFalse() + { + $object = $this->prophesize(StorageObject::class); + $object->exists()->willReturn(true); + $this->bucket->object('existing_file.txt')->willReturn($object->reveal()); + $fp = @fopen('gs://my_bucket/existing_file.txt', 'x'); $this->assertFalse($fp); } + /** + * @group storageWrite + */ + public function testOpeningNonExistentFileWithXModeSucceeds() + { + $object = $this->prophesize(StorageObject::class); + $object->exists()->willReturn(false); + $this->bucket->object('new_file.txt')->willReturn($object->reveal()); + + $uploader = $this->prophesize(StreamableUploader::class); + $uploader->upload()->shouldBeCalled(); + $uploader->getResumeUri()->willReturn('https://resume-uri/'); + + $this->bucket->getStreamableUploader(Argument::any(), Argument::withEntry('ifGenerationMatch', 0)) + ->willReturn($uploader->reveal()); + + $fp = fopen('gs://my_bucket/new_file.txt', 'x'); + $this->assertIsResource($fp); + fwrite($fp, "some data"); + fclose($fp); + } + + /** + * @group storageWrite + */ + public function testOpeningNonExistentFileWithXbModeSucceeds() + { + $object = $this->prophesize(StorageObject::class); + $object->exists()->willReturn(false); + $this->bucket->object('new_file.txt')->willReturn($object->reveal()); + + $uploader = $this->prophesize(StreamableUploader::class); + $uploader->upload()->shouldBeCalled(); + $uploader->getResumeUri()->willReturn('https://resume-uri/'); + + $this->bucket->getStreamableUploader(Argument::any(), Argument::withEntry('ifGenerationMatch', 0)) + ->willReturn($uploader->reveal()); + + $fp = fopen('gs://my_bucket/new_file.txt', 'xb'); + $this->assertIsResource($fp); + fwrite($fp, "some data"); + fclose($fp); + } + /** * @group storageRead */