Skip to content

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
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
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
Dec 7, 2015
623a98b
PHPLIB-147: Implement GridFS download
Dec 9, 2015
c787545
PHPLIB-148: Implement file deletion
Dec 10, 2015
2f76c46
PHPLIB-154: implement downloadByName
Dec 21, 2015
22e0480
PHPLIB-149: implement find on files collection
Dec 21, 2015
dacd44d
Response to PR comments to mongodb-php-library pull 57
Dec 28, 2015
8bcdab2
restructure some aspects to be more inline with SPEC and add addition…
Jan 5, 2016
d4fded2
additional tests: WIP
Jan 5, 2016
6cc4a58
added many many more tests, added getIdFromStream
Jan 6, 2016
1171293
Stream registering is now totally transparent to the user
Jan 6, 2016
60d5e88
added 'Abort' to clean up according to spec
Jan 6, 2016
21fb2c9
Rename function added to bucket
Jan 6, 2016
65fde2b
Straighten out abort method
Jan 7, 2016
ece9a00
Added corrupt chunk tests
Jan 7, 2016
a81bb85
removed all of the unused 'use' files
Jan 7, 2016
ead7cf1
uncommented argument check
Jan 7, 2016
e534225
BucketReadWriter and GridFsStream are obsolete
jmikola Jan 11, 2016
73a129e
Apply CS fixes and refactor GridFS classes
jmikola Jan 11, 2016
1706d0a
Replace public $id property with getter method
jmikola Jan 11, 2016
59e7ebd
Replace large GridFS fixture with generated temp file
jmikola Jan 11, 2016
8277ec3
Merge pull request #1 from jmikola/phplib-114-review
williambanfield Jan 12, 2016
4b947a3
PHPLIB-158: delete removes files document first
Jan 12, 2016
cb2a7ee
PHPLIB-159: rename uses UpdateOne() per the spec
Jan 12, 2016
87d08a6
PHPLIB-160: implement drop() on bucket
Jan 12, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Exception/GridFSCorruptFileException.php
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
{
}
11 changes: 11 additions & 0 deletions src/Exception/GridFSFileNotFoundException.php
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));
}
}
367 changes: 367 additions & 0 deletions src/GridFS/Bucket.php
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 = [])
Copy link
Member

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.

{
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);
}
}
Loading