Skip to content

Commit 46864db

Browse files
committed
Refactor GridFS internals
The Bucket's public API remains unchanged. More logic has been moved into the CollectionWrapper and the stream classes have also been renamed. The StreamWrapper's registration has been changed to allow a configurable protocol (Bucket defaults to "gridfs"). While file paths are generated for readable and writable streams (now in a more robust manner), necessary arguments are passed through context options. We may decide to simplify the path for readable streams down the line, unless the current path is beneficial for debugging. Closes #167
1 parent 2b93dab commit 46864db

File tree

9 files changed

+544
-236
lines changed

9 files changed

+544
-236
lines changed

src/GridFS/Bucket.php

Lines changed: 128 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use MongoDB\Exception\InvalidArgumentException;
1111
use MongoDB\GridFS\Exception\FileNotFoundException;
1212
use MongoDB\Operation\Find;
13+
use stdClass;
1314

1415
/**
1516
* Bucket provides a public API for interacting with the GridFS files and chunks
@@ -19,8 +20,8 @@
1920
*/
2021
class Bucket
2122
{
22-
private static $streamWrapper;
2323
private static $defaultChunkSizeBytes = 261120;
24+
private static $streamWrapperProtocol = 'gridfs';
2425

2526
private $collectionWrapper;
2627
private $databaseName;
@@ -75,7 +76,7 @@ public function __construct(Manager $manager, $databaseName, array $options = []
7576
$collectionOptions = array_intersect_key($options, ['readPreference' => 1, 'writeConcern' => 1]);
7677

7778
$this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions);
78-
$this->registerStreamWrapper($manager);
79+
$this->registerStreamWrapper();
7980
}
8081

8182
/**
@@ -89,14 +90,12 @@ public function __construct(Manager $manager, $databaseName, array $options = []
8990
*/
9091
public function delete(ObjectId $id)
9192
{
92-
$file = $this->collectionWrapper->getFilesCollection()->findOne(['_id' => $id]);
93-
$this->collectionWrapper->getFilesCollection()->deleteOne(['_id' => $id]);
94-
$this->collectionWrapper->getChunksCollection()->deleteMany(['files_id' => $id]);
93+
$file = $this->collectionWrapper->findFileById($id);
94+
$this->collectionWrapper->deleteFileAndChunksById($id);
9595

9696
if ($file === null) {
97-
throw FileNotFoundException::byId($id, $this->collectionWrapper->getFilesCollection()->getNameSpace());
97+
throw FileNotFoundException::byId($id, $this->getFilesNamespace());
9898
}
99-
10099
}
101100

102101
/**
@@ -108,17 +107,14 @@ public function delete(ObjectId $id)
108107
*/
109108
public function downloadToStream(ObjectId $id, $destination)
110109
{
111-
$file = $this->collectionWrapper->getFilesCollection()->findOne(
112-
['_id' => $id],
113-
['typeMap' => ['root' => 'stdClass']]
114-
);
110+
$file = $this->collectionWrapper->findFileById($id);
115111

116112
if ($file === null) {
117-
throw FileNotFoundException::byId($id, $this->collectionWrapper->getFilesCollection()->getNameSpace());
113+
throw FileNotFoundException::byId($id, $this->getFilesNamespace());
118114
}
119115

120-
$gridFsStream = new GridFSDownload($this->collectionWrapper, $file);
121-
$gridFsStream->downloadToStream($destination);
116+
$stream = new ReadableStream($this->collectionWrapper, $file);
117+
$stream ->downloadToStream($destination);
122118
}
123119

124120
/**
@@ -148,23 +144,29 @@ public function downloadToStream(ObjectId $id, $destination)
148144
public function downloadToStreamByName($filename, $destination, array $options = [])
149145
{
150146
$options += ['revision' => -1];
151-
$file = $this->findFileRevision($filename, $options['revision']);
152-
$gridFsStream = new GridFSDownload($this->collectionWrapper, $file);
153-
$gridFsStream->downloadToStream($destination);
147+
148+
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']);
149+
150+
if ($file === null) {
151+
throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace());
152+
}
153+
154+
$stream = new ReadableStream($this->collectionWrapper, $file);
155+
$stream->downloadToStream($destination);
154156
}
155157

156158
/**
157-
* Drops the files and chunks collection associated with GridFS this bucket
158-
*
159-
*/
160-
159+
* Drops the files and chunks collections associated with this GridFS
160+
* bucket.
161+
*/
161162
public function drop()
162163
{
163164
$this->collectionWrapper->dropCollections();
164165
}
165166

166167
/**
167-
* Find files from the GridFS bucket's files collection.
168+
* Finds documents from the GridFS bucket's files collection matching the
169+
* query.
168170
*
169171
* @see Find::__construct() for supported options
170172
* @param array|object $filter Query by which to filter documents
@@ -173,10 +175,10 @@ public function drop()
173175
*/
174176
public function find($filter, array $options = [])
175177
{
176-
return $this->collectionWrapper->getFilesCollection()->find($filter, $options);
178+
return $this->collectionWrapper->findFiles($filter, $options);
177179
}
178180

179-
public function getCollectionsWrapper()
181+
public function getCollectionWrapper()
180182
{
181183
return $this->collectionWrapper;
182184
}
@@ -200,7 +202,7 @@ public function getIdFromStream($stream)
200202
return $metadata['wrapper_data']->getId();
201203
}
202204

203-
return;
205+
// TODO: Throw if we cannot access the ID
204206
}
205207

206208
/**
@@ -212,13 +214,10 @@ public function getIdFromStream($stream)
212214
*/
213215
public function openDownloadStream(ObjectId $id)
214216
{
215-
$file = $this->collectionWrapper->getFilesCollection()->findOne(
216-
['_id' => $id],
217-
['typeMap' => ['root' => 'stdClass']]
218-
);
217+
$file = $this->collectionWrapper->findFileById($id);
219218

220219
if ($file === null) {
221-
throw FileNotFoundException::byId($id, $this->collectionWrapper->getFilesCollection()->getNameSpace());
220+
throw FileNotFoundException::byId($id, $this->getFilesNamespace());
222221
}
223222

224223
return $this->openDownloadStreamByFile($file);
@@ -251,7 +250,12 @@ public function openDownloadStream(ObjectId $id)
251250
public function openDownloadStreamByName($filename, array $options = [])
252251
{
253252
$options += ['revision' => -1];
254-
$file = $this->findFileRevision($filename, $options['revision']);
253+
254+
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']);
255+
256+
if ($file === null) {
257+
throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace());
258+
}
255259

256260
return $this->openDownloadStreamByFile($file);
257261
}
@@ -265,36 +269,51 @@ public function openDownloadStreamByName($filename, array $options = [])
265269
* bucket's chunk size.
266270
*
267271
* @param string $filename File name
268-
* @param array $options Stream options
272+
* @param array $options Upload options
269273
* @return resource
270274
*/
271275
public function openUploadStream($filename, array $options = [])
272276
{
273277
$options += ['chunkSizeBytes' => $this->options['chunkSizeBytes']];
274278

275-
$streamOptions = [
276-
'collectionWrapper' => $this->collectionWrapper,
277-
'uploadOptions' => $options,
278-
];
279-
280-
$context = stream_context_create(['gridfs' => $streamOptions]);
279+
$path = $this->createPathForUpload();
280+
$context = stream_context_create([
281+
self::$streamWrapperProtocol => [
282+
'collectionWrapper' => $this->collectionWrapper,
283+
'filename' => $filename,
284+
'options' => $options,
285+
],
286+
]);
281287

282-
return fopen(sprintf('gridfs://%s/%s', $this->databaseName, $filename), 'w', false, $context);
288+
return fopen($path, 'w', false, $context);
283289
}
284290

285291
/**
286292
* Renames the GridFS file with the specified ID.
287293
*
288294
* @param ObjectId $id ID of the file to rename
289295
* @param string $newFilename New file name
290-
* @throws GridFSFileNotFoundException
296+
* @throws FileNotFoundException
291297
*/
292298
public function rename(ObjectId $id, $newFilename)
293299
{
294-
$filesCollection = $this->collectionWrapper->getFilesCollection();
295-
$result = $filesCollection->updateOne(['_id' => $id], ['$set' => ['filename' => $newFilename]]);
296-
if($result->getModifiedCount() == 0) {
297-
throw FileNotFoundException::byId($id, $this->collectionWrapper->getFilesCollection()->getNameSpace());
300+
$updateResult = $this->collectionWrapper->updateFilenameForId($id, $newFilename);
301+
302+
if ($updateResult->getModifiedCount() === 1) {
303+
return;
304+
}
305+
306+
/* If the update resulted in no modification, it's possible that the
307+
* file did not exist, in which case we must raise an error. Checking
308+
* the write result's matched count will be most efficient, but fall
309+
* back to a findOne operation if necessary (i.e. legacy writes).
310+
*/
311+
$found = $updateResult->getMatchedCount() !== null
312+
? $updateResult->getMatchedCount() === 1
313+
: $this->collectionWrapper->findFileById($id) !== null;
314+
315+
if ( ! $found) {
316+
throw FileNotFoundException::byId($id, $this->getFilesNamespace());
298317
}
299318
}
300319

@@ -310,61 +329,93 @@ public function rename(ObjectId $id, $newFilename)
310329
* @param resource $source Readable stream
311330
* @param array $options Stream options
312331
* @return ObjectId
332+
* @throws InvalidArgumentException
313333
*/
314334
public function uploadFromStream($filename, $source, array $options = [])
315335
{
316336
$options += ['chunkSizeBytes' => $this->options['chunkSizeBytes']];
317-
$gridFsStream = new GridFSUpload($this->collectionWrapper, $filename, $options);
318337

319-
return $gridFsStream->uploadFromStream($source);
338+
$stream = new WritableStream($this->collectionWrapper, $filename, $options);
339+
340+
return $stream->uploadFromStream($source);
320341
}
321342

322-
private function findFileRevision($filename, $revision)
343+
/**
344+
* Creates a path for an existing GridFS file.
345+
*
346+
* @param stdClass $file GridFS file document
347+
* @return string
348+
*/
349+
private function createPathForFile(stdClass $file)
323350
{
324-
if ($revision < 0) {
325-
$skip = abs($revision) - 1;
326-
$sortOrder = -1;
351+
if ( ! is_object($file->_id) || method_exists($file->_id, '__toString')) {
352+
$id = (string) $file->_id;
327353
} else {
328-
$skip = $revision;
329-
$sortOrder = 1;
354+
$id = \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP(['_id' => $file->_id]));
330355
}
331356

332-
$filesCollection = $this->collectionWrapper->getFilesCollection();
333-
$file = $filesCollection->findOne(
334-
['filename' => $filename],
335-
[
336-
'skip' => $skip,
337-
'sort' => ['uploadDate' => $sortOrder],
338-
'typeMap' => ['root' => 'stdClass'],
339-
]
357+
return sprintf(
358+
'%s://%s/%s.files/%s',
359+
self::$streamWrapperProtocol,
360+
urlencode($this->databaseName),
361+
urlencode($this->options['bucketName']),
362+
urlencode($id)
340363
);
341-
342-
if ($file === null) {
343-
throw FileNotFoundException::byFilenameAndRevision($filename, $revision, $filesCollection->getNameSpace());
344-
}
345-
346-
return $file;
347364
}
348365

349-
private function openDownloadStreamByFile($file)
366+
/**
367+
* Creates a path for a new GridFS file, which does not yet have an ID.
368+
*
369+
* @return string
370+
*/
371+
private function createPathForUpload()
350372
{
351-
$options = [
352-
'collectionWrapper' => $this->collectionWrapper,
353-
'file' => $file,
354-
];
373+
return sprintf(
374+
'%s://%s/%s.files',
375+
self::$streamWrapperProtocol,
376+
urlencode($this->databaseName),
377+
urlencode($this->options['bucketName'])
378+
);
379+
}
355380

356-
$context = stream_context_create(['gridfs' => $options]);
381+
/**
382+
* Returns the names of the files collection.
383+
*
384+
* @return string
385+
*/
386+
private function getFilesNamespace()
387+
{
388+
return sprintf('%s.%s.files', $this->databaseName, $this->options['bucketName']);
389+
}
357390

358-
return fopen(sprintf('gridfs://%s/%s', $this->databaseName, $file->filename), 'r', false, $context);
391+
/**
392+
* Opens a readable stream for the GridFS file.
393+
*
394+
* @param stdClass $file GridFS file document
395+
* @return resource
396+
*/
397+
private function openDownloadStreamByFile(stdClass $file)
398+
{
399+
$path = $this->createPathForFile($file);
400+
$context = stream_context_create([
401+
self::$streamWrapperProtocol => [
402+
'collectionWrapper' => $this->collectionWrapper,
403+
'file' => $file,
404+
],
405+
]);
406+
407+
return fopen($path, 'r', false, $context);
359408
}
360409

361-
private function registerStreamWrapper(Manager $manager)
410+
/**
411+
* Registers the GridFS stream wrapper if it is not already registered.
412+
*/
413+
private function registerStreamWrapper()
362414
{
363-
if (isset(self::$streamWrapper)) {
415+
if (in_array(self::$streamWrapperProtocol, stream_get_wrappers())) {
364416
return;
365417
}
366418

367-
self::$streamWrapper = new StreamWrapper();
368-
self::$streamWrapper->register($manager);
419+
StreamWrapper::register(self::$streamWrapperProtocol);
369420
}
370421
}

0 commit comments

Comments
 (0)