Skip to content

Commit 401bbd7

Browse files
authored
PHPLIB-1324 Implement rename for GridFS stream wrapper (#1207)
Optimize rename using a single updateMany command instead of doing the implementation suggested in the spec https://github.com/mongodb/specifications/blob/0b47194538aa817978fae0f77f684f6d5e62ebab/source/gridfs/gridfs-spec.rst#renaming-stored-files
1 parent 4c6eb66 commit 401bbd7

File tree

6 files changed

+90
-1
lines changed

6 files changed

+90
-1
lines changed

docs/reference/method/MongoDBGridFSBucket-registerGlobalStreamWrapperAlias.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ Supported stream functions:
4747
- :php:`filemtime() <filemtime>`
4848
- :php:`filesize() <filesize>`
4949
- :php:`file() <file>`
50-
- :php:`fopen() <fopen>` (with "r", "rb", "w", and "wb" modes)
50+
- :php:`fopen() <fopen>` with "r", "rb", "w", and "wb" modes
51+
- :php:`rename() <rename>` rename all revisions of a file in the same bucket
5152

5253
In read mode, the stream context can contain the option ``gridfs['revision']``
5354
to specify the revision number of the file to read. If omitted, the most recent

src/GridFS/CollectionWrapper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ public function insertFile($file): void
253253
$this->filesCollection->insertOne($file);
254254
}
255255

256+
/**
257+
* Updates the filename field in the file document for all the files with a given filename.
258+
*/
259+
public function updateFilenameForFilename(string $filename, string $newFilename): UpdateResult
260+
{
261+
return $this->filesCollection->updateMany(
262+
['filename' => $filename],
263+
['$set' => ['filename' => $newFilename]],
264+
);
265+
}
266+
256267
/**
257268
* Updates the filename field in the file document for a given ID.
258269
*

src/GridFS/Exception/LogicException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,14 @@ public static function openModeNotSupported(string $mode): self
6767
{
6868
return new self(sprintf('Mode "%s" is not supported by "gridfs://" files. Use one of "r", "rb", "w", or "wb".', $mode));
6969
}
70+
71+
/**
72+
* Thrown when the origin and destination paths are not in the same bucket.
73+
*
74+
* @internal
75+
*/
76+
public static function renamePathMismatch(string $from, string $to): self
77+
{
78+
return new self(sprintf('Cannot rename "%s" to "%s" because they are not in the same GridFS bucket.', $from, $to));
79+
}
7080
}

src/GridFS/ReadableStream.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
use MongoDB\Driver\CursorInterface;
2323
use MongoDB\Exception\InvalidArgumentException;
2424
use MongoDB\GridFS\Exception\CorruptFileException;
25+
use MongoDB\GridFS\Exception\LogicException;
2526

2627
use function assert;
2728
use function ceil;
2829
use function floor;
2930
use function is_integer;
3031
use function is_object;
32+
use function is_string;
3133
use function property_exists;
3234
use function sprintf;
3335
use function strlen;
@@ -176,6 +178,20 @@ public function readBytes(int $length): string
176178
return $data;
177179
}
178180

181+
/**
182+
* Rename all revisions of the file.
183+
*/
184+
public function rename(string $newFilename): bool
185+
{
186+
if (! isset($this->file->filename) || ! is_string($this->file->filename)) {
187+
throw new LogicException('Cannot rename file without a filename');
188+
}
189+
190+
$this->collectionWrapper->updateFilenameForFilename($this->file->filename, $newFilename);
191+
192+
return true;
193+
}
194+
179195
/**
180196
* Seeks the chunk and buffer offsets for the next read operation.
181197
*

src/GridFS/StreamWrapper.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
use MongoDB\GridFS\Exception\FileNotFoundException;
2323
use MongoDB\GridFS\Exception\LogicException;
2424

25+
use function array_slice;
2526
use function assert;
2627
use function explode;
28+
use function implode;
2729
use function in_array;
2830
use function is_array;
2931
use function is_integer;
3032
use function is_resource;
33+
use function str_starts_with;
3134
use function stream_context_get_options;
3235
use function stream_get_wrappers;
3336
use function stream_wrapper_register;
@@ -90,6 +93,30 @@ public static function register(string $protocol = 'gridfs'): void
9093
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
9194
}
9295

96+
/**
97+
* Rename all revisions of a filename.
98+
*
99+
* @return bool True on success or false on failure.
100+
*/
101+
public function rename(string $fromPath, string $toPath): bool
102+
{
103+
$prefix = implode('/', array_slice(explode('/', $fromPath, 4), 0, 3)) . '/';
104+
if (! str_starts_with($toPath, $prefix)) {
105+
throw LogicException::renamePathMismatch($fromPath, $toPath);
106+
}
107+
108+
try {
109+
$this->stream_open($fromPath, 'r', 0, $openedPath);
110+
} catch (FileNotFoundException $e) {
111+
return false;
112+
}
113+
114+
$newName = explode('/', $toPath, 4)[3] ?? '';
115+
assert($this->stream instanceof ReadableStream);
116+
117+
return $this->stream->rename($newName);
118+
}
119+
93120
/**
94121
* @see Bucket::resolveStreamContext()
95122
*

tests/GridFS/StreamWrapperFunctionalTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use function is_dir;
2626
use function is_file;
2727
use function is_link;
28+
use function rename;
2829
use function stream_context_create;
2930
use function stream_get_contents;
3031
use function time;
@@ -363,4 +364,27 @@ public function testCopy(): void
363364
$this->assertSame('foobar', file_get_contents($path . '.copy'));
364365
$this->assertSame('foobar', file_get_contents($path));
365366
}
367+
368+
public function testRenameAllRevisions(): void
369+
{
370+
$this->bucket->registerGlobalStreamWrapperAlias('bucket');
371+
$path = 'gridfs://bucket/filename';
372+
373+
$this->assertSame(6, file_put_contents($path, 'foobar'));
374+
$this->assertSame(6, file_put_contents($path, 'foobar'));
375+
$this->assertSame(6, file_put_contents($path, 'foobar'));
376+
377+
$this->assertTrue(rename($path, $path . '.renamed'));
378+
$this->assertTrue(file_exists($path . '.renamed'));
379+
$this->assertFalse(file_exists($path));
380+
$this->assertSame('foobar', file_get_contents($path . '.renamed'));
381+
}
382+
383+
public function testRenamePathMismatch(): void
384+
{
385+
$this->expectException(LogicException::class);
386+
$this->expectExceptionMessage('Cannot rename "gridfs://bucket/filename" to "gridfs://other/newname" because they are not in the same GridFS bucket.');
387+
388+
rename('gridfs://bucket/filename', 'gridfs://other/newname');
389+
}
366390
}

0 commit comments

Comments
 (0)