diff --git a/Storage/VERSION b/Storage/VERSION index ba0a719118ce..227cea215648 100644 --- a/Storage/VERSION +++ b/Storage/VERSION @@ -1 +1 @@ -1.51.0 +2.0.0 diff --git a/Storage/src/StorageClient.php b/Storage/src/StorageClient.php index 99cce3f6abdf..8d870ec83767 100644 --- a/Storage/src/StorageClient.php +++ b/Storage/src/StorageClient.php @@ -47,7 +47,7 @@ class StorageClient use ArrayTrait; use ClientTrait; - const VERSION = '1.51.0'; + const VERSION = '2.0.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control'; const READ_ONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only'; 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..97367a6f85bc 100644 --- a/Storage/tests/Unit/StreamWrapperTest.php +++ b/Storage/tests/Unit/StreamWrapperTest.php @@ -83,10 +83,45 @@ 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 storageRead */ diff --git a/composer.json b/composer.json index 2508aeab89c6..4cee27563e3e 100644 --- a/composer.json +++ b/composer.json @@ -254,7 +254,7 @@ "google/cloud-spanner": "2.7.0", "google/cloud-speech": "2.5.0", "google/cloud-sql-admin": "1.8.0", - "google/cloud-storage": "1.51.0", + "google/cloud-storage": "2.0.0", "google/cloud-storage-control": "1.8.0", "google/cloud-storage-transfer": "2.4.0", "google/cloud-storagebatchoperations": "0.7.0",