diff --git a/src/CloudStorage/AbstractCloudStorageStreamWrapper.php b/src/CloudStorage/AbstractCloudStorageStreamWrapper.php index e716c0f..08983c7 100644 --- a/src/CloudStorage/AbstractCloudStorageStreamWrapper.php +++ b/src/CloudStorage/AbstractCloudStorageStreamWrapper.php @@ -332,9 +332,18 @@ public function stream_lock(): bool * * @see https://www.php.net/manual/en/streamwrapper.stream-metadata.php */ - public function stream_metadata(): bool + public function stream_metadata(string $path, int $option): bool { - return false; + return STREAM_META_TOUCH === $option ? $this->call(function () use ($path) { + $client = $this->getClient(); + $key = $this->parsePath($path); + + if (!$client->objectExists($key)) { + $client->putObject($key, '', $this->getAcl()); + } + + $this->removeCacheValue($path); + }) : true; } /** diff --git a/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperPhpTestCase.php b/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperPhpTestCase.php index fafbab3..1653790 100644 --- a/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperPhpTestCase.php +++ b/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperPhpTestCase.php @@ -496,6 +496,33 @@ public function testThrowsExceptionWhenContextHasNoClient() ])); } + public function testTouchCreatesObjectIfItDoesntExist() + { + $this->client->expects($this->once()) + ->method('objectExists') + ->with($this->identicalTo('/file.ext')) + ->willReturn(false); + + $this->client->expects($this->once()) + ->method('putObject') + ->with($this->identicalTo('/file.ext'), $this->identicalTo(''), $this->identicalTo($this->getAcl())); + + $this->assertTrue(touch("{$this->getProtocol()}:///file.ext")); + } + + public function testTouchDoesntCreateObjectIfItExists() + { + $this->client->expects($this->once()) + ->method('objectExists') + ->with($this->identicalTo('/file.ext')) + ->willReturn(false); + + $this->client->expects($this->once()) + ->method('putObject'); + + $this->assertTrue(touch("{$this->getProtocol()}:///file.ext")); + } + public function testTruncatesFile() { $this->client->expects($this->once()) diff --git a/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperS3TestCase.php b/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperS3TestCase.php index a427f65..f9fe882 100644 --- a/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperS3TestCase.php +++ b/tests/Integration/CloudStorage/AbstractCloudStorageStreamWrapperS3TestCase.php @@ -106,6 +106,39 @@ public function testMkdirAndRmdir() $this->assertFalse($this->client->objectExists($directoryPath.'/')); } + public function testTouchCreatesEmptyFileIfFileDoesntExist() + { + $relativePath = '/'.basename(tempnam(sys_get_temp_dir(), 'ymir-').'.txt'); + $s3FilePath = "{$this->getProtocol()}://".$relativePath; + + $this->assertFalse(file_exists($s3FilePath)); + + $this->assertTrue(touch($s3FilePath)); + + $this->assertTrue(file_exists($s3FilePath)); + $this->assertSame('', file_get_contents($s3FilePath)); + + $this->client->deleteObject($relativePath); + } + + public function testTouchDoesNothingIfFileExists() + { + $relativePath = '/'.basename(tempnam(sys_get_temp_dir(), 'ymir-').'.txt'); + $s3FilePath = "{$this->getProtocol()}://".$relativePath; + + $this->assertFalse(file_exists($s3FilePath)); + + file_put_contents($s3FilePath, 'foo'); + + $this->assertTrue(file_exists($s3FilePath)); + + $this->assertTrue(touch($s3FilePath)); + + $this->assertSame('foo', file_get_contents($s3FilePath)); + + $this->client->deleteObject($relativePath); + } + public function testTruncateExistingFile() { $relativePath = '/'.basename(tempnam(sys_get_temp_dir(), 'ymir-').'.txt'); diff --git a/tests/Unit/CloudStorage/AbstractCloudStorageStreamWrapperTestCase.php b/tests/Unit/CloudStorage/AbstractCloudStorageStreamWrapperTestCase.php index 44fde29..fe5be2c 100644 --- a/tests/Unit/CloudStorage/AbstractCloudStorageStreamWrapperTestCase.php +++ b/tests/Unit/CloudStorage/AbstractCloudStorageStreamWrapperTestCase.php @@ -643,9 +643,44 @@ public function testStreamLock() $this->assertFalse($this->getStreamWrapperObject()->stream_lock()); } - public function testStreamMetadata() + public function testStreamMetadataWithNonTouchOption() { - $this->assertFalse($this->getStreamWrapperObject()->stream_metadata()); + $this->assertTrue($this->getStreamWrapperObject()->stream_metadata("{$this->getProtocol()}:///foo.txt", STREAM_META_GROUP)); + } + + public function testStreamMetadataWithTouchOptionWhenObjectDoesntExist() + { + $client = $this->getCloudStorageClientInterfaceMock(); + + $client->expects($this->once()) + ->method('objectExists') + ->with($this->identicalTo('/foo.txt')) + ->willReturn(false); + + $client->expects($this->once()) + ->method('putObject') + ->with($this->identicalTo('/foo.txt'), $this->identicalTo(''), $this->identicalTo($this->getAcl())); + + $this->getStreamWrapperClass()::register($client, new \ArrayObject()); + + $this->assertTrue($this->getStreamWrapperObject()->stream_metadata("{$this->getProtocol()}:///foo.txt", STREAM_META_TOUCH)); + } + + public function testStreamMetadataWithTouchOptionWhenObjectExists() + { + $client = $this->getCloudStorageClientInterfaceMock(); + + $client->expects($this->once()) + ->method('objectExists') + ->with($this->identicalTo('/foo.txt')) + ->willReturn(true); + + $client->expects($this->never()) + ->method('putObject'); + + $this->getStreamWrapperClass()::register($client, new \ArrayObject()); + + $this->assertTrue($this->getStreamWrapperObject()->stream_metadata("{$this->getProtocol()}:///foo.txt", STREAM_META_TOUCH)); } public function testStreamOpenWithInvalidMode()