-
Notifications
You must be signed in to change notification settings - Fork 266
PHPLIB-114: Implement GridFS specification #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
56023b0
PHPLIB-146: Implement GridFS upload, plus initial GridFS commit
623a98b
PHPLIB-147: Implement GridFS download
c787545
PHPLIB-148: Implement file deletion
2f76c46
PHPLIB-154: implement downloadByName
22e0480
PHPLIB-149: implement find on files collection
dacd44d
Response to PR comments to mongodb-php-library pull 57
8bcdab2
restructure some aspects to be more inline with SPEC and add addition…
d4fded2
additional tests: WIP
6cc4a58
added many many more tests, added getIdFromStream
1171293
Stream registering is now totally transparent to the user
60d5e88
added 'Abort' to clean up according to spec
21fb2c9
Rename function added to bucket
65fde2b
Straighten out abort method
ece9a00
Added corrupt chunk tests
a81bb85
removed all of the unused 'use' files
ead7cf1
uncommented argument check
e534225
BucketReadWriter and GridFsStream are obsolete
jmikola 73a129e
Apply CS fixes and refactor GridFS classes
jmikola 1706d0a
Replace public $id property with getter method
jmikola 59e7ebd
Replace large GridFS fixture with generated temp file
jmikola 8277ec3
Merge pull request #1 from jmikola/phplib-114-review
williambanfield 4b947a3
PHPLIB-158: delete removes files document first
cb2a7ee
PHPLIB-159: rename uses UpdateOne() per the spec
87d08a6
PHPLIB-160: implement drop() on bucket
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace MongoDB\Exception; | ||
|
||
class GridFSCorruptFileException extends \MongoDB\Driver\Exception\RuntimeException implements Exception | ||
{ | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
namespace MongoDB\Exception; | ||
|
||
class GridFSFileNotFoundException extends \MongoDB\Driver\Exception\RuntimeException implements Exception | ||
{ | ||
public function __construct($filename, $namespace) | ||
{ | ||
parent::__construct(sprintf('Unable to find file "%s" in namespace "%s"', $filename, $namespace)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
<?php | ||
|
||
namespace MongoDB\GridFS; | ||
|
||
use MongoDB\BSON\ObjectId; | ||
use MongoDB\Driver\Cursor; | ||
use MongoDB\Driver\Manager; | ||
use MongoDB\Exception\GridFSFileNotFoundException; | ||
use MongoDB\Exception\InvalidArgumentTypeException; | ||
use MongoDB\Operation\Find; | ||
|
||
/** | ||
* Bucket provides a public API for interacting with the GridFS files and chunks | ||
* collections. | ||
* | ||
* @api | ||
*/ | ||
class Bucket | ||
{ | ||
private static $streamWrapper; | ||
|
||
private $collectionsWrapper; | ||
private $databaseName; | ||
private $options; | ||
|
||
/** | ||
* Constructs a GridFS bucket. | ||
* | ||
* Supported options: | ||
* | ||
* * bucketName (string): The bucket name, which will be used as a prefix | ||
* for the files and chunks collections. Defaults to "fs". | ||
* | ||
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to | ||
* 261120 (i.e. 255 KiB). | ||
* | ||
* * readPreference (MongoDB\Driver\ReadPreference): Read preference. | ||
* | ||
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern. | ||
* | ||
* @param Manager $manager Manager instance from the driver | ||
* @param string $databaseName Database name | ||
* @param array $options Bucket options | ||
* @throws InvalidArgumentException | ||
*/ | ||
public function __construct(Manager $manager, $databaseName, array $options = []) | ||
{ | ||
$options += [ | ||
'bucketName' => 'fs', | ||
'chunkSizeBytes' => 261120, | ||
]; | ||
|
||
if (isset($options['bucketName']) && ! is_string($options['bucketName'])) { | ||
throw new InvalidArgumentTypeException('"bucketName" option', $options['bucketName'], 'string'); | ||
} | ||
|
||
if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) { | ||
throw new InvalidArgumentTypeException('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer'); | ||
} | ||
|
||
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) { | ||
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference'); | ||
} | ||
|
||
if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) { | ||
throw new InvalidArgumentTypeException('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern'); | ||
} | ||
|
||
$this->databaseName = (string) $databaseName; | ||
$this->options = $options; | ||
|
||
$collectionOptions = array_intersect_key($options, ['readPreference' => 1, 'writeConcern' => 1]); | ||
|
||
$this->collectionsWrapper = new GridFSCollectionsWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions); | ||
$this->registerStreamWrapper($manager); | ||
} | ||
|
||
/** | ||
* Delete a file from the GridFS bucket. | ||
* | ||
* If the files collection document is not found, this method will still | ||
* attempt to delete orphaned chunks. | ||
* | ||
* @param ObjectId $id ObjectId of the file | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function delete(ObjectId $id) | ||
{ | ||
$file = $this->collectionsWrapper->getFilesCollection()->findOne(['_id' => $id]); | ||
$this->collectionsWrapper->getFilesCollection()->deleteOne(['_id' => $id]); | ||
$this->collectionsWrapper->getChunksCollection()->deleteMany(['files_id' => $id]); | ||
|
||
if ($file === null) { | ||
throw new GridFSFileNotFoundException($id, $this->collectionsWrapper->getFilesCollection()->getNameSpace()); | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Writes the contents of a GridFS file to a writable stream. | ||
* | ||
* @param ObjectId $id ObjectId of the file | ||
* @param resource $destination Writable Stream | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function downloadToStream(ObjectId $id, $destination) | ||
{ | ||
$file = $this->collectionsWrapper->getFilesCollection()->findOne( | ||
['_id' => $id], | ||
['typeMap' => ['root' => 'stdClass']] | ||
); | ||
|
||
if ($file === null) { | ||
throw new GridFSFileNotFoundException($id, $this->collectionsWrapper->getFilesCollection()->getNameSpace()); | ||
} | ||
|
||
$gridFsStream = new GridFSDownload($this->collectionsWrapper, $file); | ||
$gridFsStream->downloadToStream($destination); | ||
} | ||
|
||
/** | ||
* Writes the contents of a GridFS file, which is selected by name and | ||
* revision, to a writable stream. | ||
* | ||
* Supported options: | ||
* | ||
* * revision (integer): Which revision (i.e. documents with the same | ||
* filename and different uploadDate) of the file to retrieve. Defaults | ||
* to -1 (i.e. the most recent revision). | ||
* | ||
* Revision numbers are defined as follows: | ||
* | ||
* * 0 = the original stored file | ||
* * 1 = the first revision | ||
* * 2 = the second revision | ||
* * etc… | ||
* * -2 = the second most recent revision | ||
* * -1 = the most recent revision | ||
* | ||
* @param string $filename File name | ||
* @param resource $destination Writable Stream | ||
* @param array $options Download options | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function downloadToStreamByName($filename, $destination, array $options = []) | ||
{ | ||
$options += ['revision' => -1]; | ||
$file = $this->findFileRevision($filename, $options['revision']); | ||
$gridFsStream = new GridFSDownload($this->collectionsWrapper, $file); | ||
$gridFsStream->downloadToStream($destination); | ||
} | ||
|
||
/** | ||
* Drops the files and chunks collection associated with GridFS this bucket | ||
* | ||
*/ | ||
|
||
public function drop() | ||
{ | ||
$this->collectionsWrapper->dropCollections(); | ||
} | ||
|
||
/** | ||
* Find files from the GridFS bucket's files collection. | ||
* | ||
* @see Find::__construct() for supported options | ||
* @param array|object $filter Query by which to filter documents | ||
* @param array $options Additional options | ||
* @return Cursor | ||
*/ | ||
public function find($filter, array $options = []) | ||
{ | ||
return $this->collectionsWrapper->getFilesCollection()->find($filter, $options); | ||
} | ||
|
||
public function getCollectionsWrapper() | ||
{ | ||
return $this->collectionsWrapper; | ||
} | ||
|
||
public function getDatabaseName() | ||
{ | ||
return $this->databaseName; | ||
} | ||
|
||
/** | ||
* Gets the ID of the GridFS file associated with a stream. | ||
* | ||
* @param resource $stream GridFS stream | ||
* @return mixed | ||
*/ | ||
public function getIdFromStream($stream) | ||
{ | ||
$metadata = stream_get_meta_data($stream); | ||
|
||
if ($metadata['wrapper_data'] instanceof StreamWrapper) { | ||
return $metadata['wrapper_data']->getId(); | ||
} | ||
|
||
return; | ||
} | ||
|
||
/** | ||
* Opens a readable stream for reading a GridFS file. | ||
* | ||
* @param ObjectId $id ObjectId of the file | ||
* @return resource | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function openDownloadStream(ObjectId $id) | ||
{ | ||
$file = $this->collectionsWrapper->getFilesCollection()->findOne( | ||
['_id' => $id], | ||
['typeMap' => ['root' => 'stdClass']] | ||
); | ||
|
||
if ($file === null) { | ||
throw new GridFSFileNotFoundException($id, $this->collectionsWrapper->getFilesCollection()->getNameSpace()); | ||
} | ||
|
||
return $this->openDownloadStreamByFile($file); | ||
} | ||
|
||
/** | ||
* Opens a readable stream stream to read a GridFS file, which is selected | ||
* by name and revision. | ||
* | ||
* Supported options: | ||
* | ||
* * revision (integer): Which revision (i.e. documents with the same | ||
* filename and different uploadDate) of the file to retrieve. Defaults | ||
* to -1 (i.e. the most recent revision). | ||
* | ||
* Revision numbers are defined as follows: | ||
* | ||
* * 0 = the original stored file | ||
* * 1 = the first revision | ||
* * 2 = the second revision | ||
* * etc… | ||
* * -2 = the second most recent revision | ||
* * -1 = the most recent revision | ||
* | ||
* @param string $filename File name | ||
* @param array $options Download options | ||
* @return resource | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function openDownloadStreamByName($filename, array $options = []) | ||
{ | ||
$options += ['revision' => -1]; | ||
$file = $this->findFileRevision($filename, $options['revision']); | ||
|
||
return $this->openDownloadStreamByFile($file); | ||
} | ||
|
||
/** | ||
* Opens a writable stream for writing a GridFS file. | ||
* | ||
* Supported options: | ||
* | ||
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the | ||
* bucket's chunk size. | ||
* | ||
* @param string $filename File name | ||
* @param array $options Stream options | ||
* @return resource | ||
*/ | ||
public function openUploadStream($filename, array $options = []) | ||
{ | ||
$options += ['chunkSizeBytes' => $this->options['chunkSizeBytes']]; | ||
|
||
$streamOptions = [ | ||
'collectionsWrapper' => $this->collectionsWrapper, | ||
'uploadOptions' => $options, | ||
]; | ||
|
||
$context = stream_context_create(['gridfs' => $streamOptions]); | ||
|
||
return fopen(sprintf('gridfs://%s/%s', $this->databaseName, $filename), 'w', false, $context); | ||
} | ||
|
||
/** | ||
* Renames the GridFS file with the specified ID. | ||
* | ||
* @param ObjectId $id ID of the file to rename | ||
* @param string $newFilename New file name | ||
* @throws GridFSFileNotFoundException | ||
*/ | ||
public function rename(ObjectId $id, $newFilename) | ||
{ | ||
$filesCollection = $this->collectionsWrapper->getFilesCollection(); | ||
$result = $filesCollection->updateOne(['_id' => $id], ['$set' => ['filename' => $newFilename]]); | ||
if($result->getModifiedCount() == 0) { | ||
throw new GridFSFileNotFoundException($id, $this->collectionsWrapper->getFilesCollection()->getNameSpace()); | ||
} | ||
} | ||
|
||
/** | ||
* Writes the contents of a readable stream to a GridFS file. | ||
* | ||
* Supported options: | ||
* | ||
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the | ||
* bucket's chunk size. | ||
* | ||
* @param string $filename File name | ||
* @param resource $source Readable stream | ||
* @param array $options Stream options | ||
* @return ObjectId | ||
*/ | ||
public function uploadFromStream($filename, $source, array $options = []) | ||
{ | ||
$options += ['chunkSizeBytes' => $this->options['chunkSizeBytes']]; | ||
$gridFsStream = new GridFSUpload($this->collectionsWrapper, $filename, $options); | ||
|
||
return $gridFsStream->uploadFromStream($source); | ||
} | ||
|
||
private function findFileRevision($filename, $revision) | ||
{ | ||
if ($revision < 0) { | ||
$skip = abs($revision) - 1; | ||
$sortOrder = -1; | ||
} else { | ||
$skip = $revision; | ||
$sortOrder = 1; | ||
} | ||
|
||
$filesCollection = $this->collectionsWrapper->getFilesCollection(); | ||
$file = $filesCollection->findOne( | ||
['filename' => $filename], | ||
[ | ||
'skip' => $skip, | ||
'sort' => ['uploadDate' => $sortOrder], | ||
'typeMap' => ['root' => 'stdClass'], | ||
] | ||
); | ||
|
||
if ($file === null) { | ||
throw new GridFSFileNotFoundException($filename, $filesCollection->getNameSpace()); | ||
} | ||
|
||
return $file; | ||
} | ||
|
||
private function openDownloadStreamByFile($file) | ||
{ | ||
$options = [ | ||
'collectionsWrapper' => $this->collectionsWrapper, | ||
'file' => $file, | ||
]; | ||
|
||
$context = stream_context_create(['gridfs' => $options]); | ||
|
||
return fopen(sprintf('gridfs://%s/%s', $this->databaseName, $file->filename), 'r', false, $context); | ||
} | ||
|
||
private function registerStreamWrapper(Manager $manager) | ||
{ | ||
if (isset(self::$streamWrapper)) { | ||
return; | ||
} | ||
|
||
self::$streamWrapper = new StreamWrapper(); | ||
self::$streamWrapper->register($manager); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per #57 (comment), I think we should default
$filter
to an empty array.