diff --git a/docs/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias.txt b/docs/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias.txt index 649915eea..4dcc79ce9 100644 --- a/docs/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias.txt +++ b/docs/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias.txt @@ -47,7 +47,8 @@ Supported stream functions: - :php:`filemtime() ` - :php:`filesize() ` - :php:`file() ` -- :php:`fopen() ` (with "r", "rb", "w", and "wb" modes) +- :php:`fopen() ` with "r", "rb", "w", and "wb" modes +- :php:`rename() ` rename all revisions of a file in the same bucket In read mode, the stream context can contain the option ``gridfs['revision']`` to specify the revision number of the file to read. If omitted, the most recent diff --git a/src/GridFS/CollectionWrapper.php b/src/GridFS/CollectionWrapper.php index 16ef8343a..31c5df7bd 100644 --- a/src/GridFS/CollectionWrapper.php +++ b/src/GridFS/CollectionWrapper.php @@ -253,6 +253,17 @@ public function insertFile($file): void $this->filesCollection->insertOne($file); } + /** + * Updates the filename field in the file document for all the files with a given filename. + */ + public function updateFilenameForFilename(string $filename, string $newFilename): UpdateResult + { + return $this->filesCollection->updateMany( + ['filename' => $filename], + ['$set' => ['filename' => $newFilename]], + ); + } + /** * Updates the filename field in the file document for a given ID. * diff --git a/src/GridFS/Exception/LogicException.php b/src/GridFS/Exception/LogicException.php index 907e27f07..a2b6378c7 100644 --- a/src/GridFS/Exception/LogicException.php +++ b/src/GridFS/Exception/LogicException.php @@ -67,4 +67,14 @@ public static function openModeNotSupported(string $mode): self { return new self(sprintf('Mode "%s" is not supported by "gridfs://" files. Use one of "r", "rb", "w", or "wb".', $mode)); } + + /** + * Thrown when the origin and destination paths are not in the same bucket. + * + * @internal + */ + public static function renamePathMismatch(string $from, string $to): self + { + return new self(sprintf('Cannot rename "%s" to "%s" because they are not in the same GridFS bucket.', $from, $to)); + } } diff --git a/src/GridFS/ReadableStream.php b/src/GridFS/ReadableStream.php index a0664e821..402f21280 100644 --- a/src/GridFS/ReadableStream.php +++ b/src/GridFS/ReadableStream.php @@ -22,12 +22,14 @@ use MongoDB\Driver\CursorInterface; use MongoDB\Exception\InvalidArgumentException; use MongoDB\GridFS\Exception\CorruptFileException; +use MongoDB\GridFS\Exception\LogicException; use function assert; use function ceil; use function floor; use function is_integer; use function is_object; +use function is_string; use function property_exists; use function sprintf; use function strlen; @@ -176,6 +178,20 @@ public function readBytes(int $length): string return $data; } + /** + * Rename all revisions of the file. + */ + public function rename(string $newFilename): bool + { + if (! isset($this->file->filename) || ! is_string($this->file->filename)) { + throw new LogicException('Cannot rename file without a filename'); + } + + $this->collectionWrapper->updateFilenameForFilename($this->file->filename, $newFilename); + + return true; + } + /** * Seeks the chunk and buffer offsets for the next read operation. * diff --git a/src/GridFS/StreamWrapper.php b/src/GridFS/StreamWrapper.php index b5a4d6d70..7a77f11bd 100644 --- a/src/GridFS/StreamWrapper.php +++ b/src/GridFS/StreamWrapper.php @@ -22,12 +22,15 @@ use MongoDB\GridFS\Exception\FileNotFoundException; use MongoDB\GridFS\Exception\LogicException; +use function array_slice; use function assert; use function explode; +use function implode; use function in_array; use function is_array; use function is_integer; use function is_resource; +use function str_starts_with; use function stream_context_get_options; use function stream_get_wrappers; use function stream_wrapper_register; @@ -90,6 +93,30 @@ public static function register(string $protocol = 'gridfs'): void stream_wrapper_register($protocol, static::class, STREAM_IS_URL); } + /** + * Rename all revisions of a filename. + * + * @return bool True on success or false on failure. + */ + public function rename(string $fromPath, string $toPath): bool + { + $prefix = implode('/', array_slice(explode('/', $fromPath, 4), 0, 3)) . '/'; + if (! str_starts_with($toPath, $prefix)) { + throw LogicException::renamePathMismatch($fromPath, $toPath); + } + + try { + $this->stream_open($fromPath, 'r', 0, $openedPath); + } catch (FileNotFoundException $e) { + return false; + } + + $newName = explode('/', $toPath, 4)[3] ?? ''; + assert($this->stream instanceof ReadableStream); + + return $this->stream->rename($newName); + } + /** * @see Bucket::resolveStreamContext() * diff --git a/tests/GridFS/StreamWrapperFunctionalTest.php b/tests/GridFS/StreamWrapperFunctionalTest.php index a2f1fe701..d9ab66552 100644 --- a/tests/GridFS/StreamWrapperFunctionalTest.php +++ b/tests/GridFS/StreamWrapperFunctionalTest.php @@ -25,6 +25,7 @@ use function is_dir; use function is_file; use function is_link; +use function rename; use function stream_context_create; use function stream_get_contents; use function time; @@ -363,4 +364,27 @@ public function testCopy(): void $this->assertSame('foobar', file_get_contents($path . '.copy')); $this->assertSame('foobar', file_get_contents($path)); } + + public function testRenameAllRevisions(): void + { + $this->bucket->registerGlobalStreamWrapperAlias('bucket'); + $path = 'gridfs://bucket/filename'; + + $this->assertSame(6, file_put_contents($path, 'foobar')); + $this->assertSame(6, file_put_contents($path, 'foobar')); + $this->assertSame(6, file_put_contents($path, 'foobar')); + + $this->assertTrue(rename($path, $path . '.renamed')); + $this->assertTrue(file_exists($path . '.renamed')); + $this->assertFalse(file_exists($path)); + $this->assertSame('foobar', file_get_contents($path . '.renamed')); + } + + public function testRenamePathMismatch(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot rename "gridfs://bucket/filename" to "gridfs://other/newname" because they are not in the same GridFS bucket.'); + + rename('gridfs://bucket/filename', 'gridfs://other/newname'); + } }