소스 검색

Merge pull request #22 from alcaeus/implement-gridfs

Implement GridFS handling
Andreas 10 년 전
부모
커밋
677a25b65a

+ 1 - 1
lib/Mongo/MongoDB.php

@@ -183,7 +183,7 @@ class MongoDB
      */
     public function getGridFS($prefix = "fs")
     {
-        return new \MongoGridFS($this, $prefix, $prefix);
+        return new \MongoGridFS($this, $prefix);
     }
 
     /**

+ 299 - 42
lib/Mongo/MongoGridFS.php

@@ -13,7 +13,10 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-class MongoGridFS extends MongoCollection {
+class MongoGridFS extends MongoCollection
+{
+    const DEFAULT_CHUNK_SIZE = 262144; // 256 kb
+
     const ASCENDING = 1;
     const DESCENDING = -1;
 
@@ -35,7 +38,12 @@ class MongoGridFS extends MongoCollection {
      */
     protected $chunksName;
 
+    /**
+     * @var MongoDB
+     */
+    private $database;
 
+    private $prefix;
 
     /**
      * Files as stored across two collections, the first containing file meta
@@ -47,33 +55,134 @@ class MongoGridFS extends MongoCollection {
      * @param string $prefix [optional] <p>Optional collection name prefix.</p>
      * @param mixed $chunks  [optional]
      * @return MongoGridFS
+     * @throws \Exception
      */
-    public function __construct($db, $prefix = "fs", $chunks = "fs") {}
+    public function __construct(MongoDB $db, $prefix = "fs", $chunks = null)
+    {
+        if ($chunks) {
+            trigger_error("The 'chunks' argument is deprecated and ignored", E_DEPRECATED);
+        }
+        if (empty($prefix)) {
+            throw new \Exception('MongoGridFS::__construct(): invalid prefix');
+        }
+
+        $this->database = $db;
+        $this->prefix = $prefix;
+        $this->filesName = $prefix . '.files';
+        $this->chunksName = $prefix . '.chunks';
+
+        $this->chunks = $db->selectCollection($this->chunksName);
+
+        parent::__construct($db, $this->filesName);
+    }
+
+    /**
+     * Delete a file from the database
+     *
+     * @link http://php.net/manual/en/mongogridfs.delete.php
+     * @param mixed $id _id of the file to remove
+     * @return boolean Returns true if the remove was successfully sent to the database.
+     */
+    public function delete($id)
+    {
+        $this->createChunksIndex();
+
+        $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
+        return parent::remove(['_id' => $id]);
+    }
 
     /**
      * Drops the files and chunks collections
      * @link http://php.net/manual/en/mongogridfs.drop.php
      * @return array The database response
      */
-    public function drop() {}
+    public function drop()
+    {
+        $this->chunks->drop();
+        return parent::drop();
+    }
 
     /**
      * @link http://php.net/manual/en/mongogridfs.find.php
      * @param array $query The query
      * @param array $fields Fields to return
+     * @param array $options Options for the find command
      * @return MongoGridFSCursor A MongoGridFSCursor
      */
-    public function find(array $query = array(), array $fields = array()) {}
+    public function find(array $query = [], array $fields = [])
+    {
+        $cursor = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
+        $cursor->setReadPreference($this->getReadPreference());
+
+        return $cursor;
+    }
+
+    /**
+     * Returns a single file matching the criteria
+     *
+     * @link http://www.php.net/manual/en/mongogridfs.findone.php
+     * @param array $query The fields for which to search.
+     * @param array $fields Fields of the results to return.
+     * @param array $options Options for the find command
+     * @return MongoGridFSFile|null
+     */
+    public function findOne(array $query = [], array $fields = [], array $options = [])
+    {
+        if (is_string($query)) {
+            $query = ['filename' => $query];
+        }
+
+        $items = iterator_to_array($this->find($query, $fields)->limit(1));
+        return current($items);
+    }
+
+    /**
+     * Retrieve a file from the database
+     *
+     * @link http://www.php.net/manual/en/mongogridfs.get.php
+     * @param mixed $id _id of the file to find.
+     * @return MongoGridFSFile|null
+     */
+    public function get($id)
+    {
+        return $this->findOne(['_id' => $id]);
+    }
 
     /**
      * Stores a file in the database
-     * @link http://php.net/manual/en/mongogridfs.storefile.php
+     *
+     * @link http://php.net/manual/en/mongogridfs.put.php
      * @param string $filename The name of the file
      * @param array $extra Other metadata to add to the file saved
-     * @param array $options Options for the store. "safe": Check that this store succeeded
+     * @param array $options An array of options for the insert operations executed against the chunks and files collections.
      * @return mixed Returns the _id of the saved object
      */
-    public function storeFile($filename, $extra = array(), $options = array()) {}
+    public function put($filename, array $extra = [], array $options = [])
+    {
+        return $this->storeFile($filename, $extra, $options);
+    }
+
+    /**
+     * Removes files from the collections
+     *
+     * @link http://www.php.net/manual/en/mongogridfs.remove.php
+     * @param array $criteria Description of records to remove.
+     * @param array $options Options for remove.
+     * @throws MongoCursorException
+     * @return boolean
+     */
+    public function remove(array $criteria = [], array $options = [])
+    {
+        $this->createChunksIndex();
+
+        $matchingFiles = parent::find($criteria, ['_id' => 1]);
+        $ids = [];
+        foreach ($matchingFiles as $file) {
+            $ids[] = $file['_id'];
+        }
+        $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
+        return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
+    }
 
     /**
      * Chunkifies and stores bytes in the database
@@ -83,60 +192,208 @@ class MongoGridFS extends MongoCollection {
      * @param array $options Options for the store. "safe": Check that this store succeeded
      * @return mixed The _id of the object saved
      */
-    public function storeBytes($bytes, $extra = array(), $options = array()) {}
+    public function storeBytes($bytes, array $extra = [], array $options = [])
+    {
+        $this->createChunksIndex();
 
-    /**
-     * Returns a single file matching the criteria
-     * @link http://www.php.net/manual/en/mongogridfs.findone.php
-     * @param array $query The fields for which to search.
-     * @param array $fields Fields of the results to return.
-     * @return MongoGridFSFile|null
-     */
-    public function findOne(array $query = array(), array $fields = array()) {}
+        $record = $extra + [
+            'length' => mb_strlen($bytes, '8bit'),
+            'md5' => md5($bytes),
+        ];
 
-    /**
-     * Removes files from the collections
-     * @link http://www.php.net/manual/en/mongogridfs.remove.php
-     * @param array $criteria Description of records to remove.
-     * @param array $options Options for remove. Valid options are: "safe"- Check that the remove succeeded.
-     * @throws MongoCursorException
-     * @return boolean
-     */
-    public function remove(array $criteria = array(), array $options = array()) {}
+        $file = $this->insertFile($record, $options);
+        $this->insertChunksFromBytes($bytes, $file);
+
+        return $file['_id'];
+    }
 
     /**
-     * Delete a file from the database
-     * @link http://php.net/manual/en/mongogridfs.delete.php
-     * @param mixed $id _id of the file to remove
-     * @return boolean Returns true if the remove was successfully sent to the database.
+     * Stores a file in the database
+     *
+     * @link http://php.net/manual/en/mongogridfs.storefile.php
+     * @param string $filename The name of the file
+     * @param array $extra Other metadata to add to the file saved
+     * @param array $options Options for the store. "safe": Check that this store succeeded
+     * @return mixed Returns the _id of the saved object
+     * @throws MongoGridFSException
+     * @throws Exception
      */
-    public function delete($id) {}
+    public function storeFile($filename, array $extra = [], array $options = [])
+    {
+        $this->createChunksIndex();
+
+        $record = $extra;
+        if (is_string($filename)) {
+            $record += [
+                'md5' => md5_file($filename),
+                'length' => filesize($filename),
+                'filename' => $filename,
+            ];
+
+            $handle = fopen($filename, 'r');
+            if (! $handle) {
+                throw new MongoGridFSException('could not open file: ' . $filename);
+            }
+        } elseif (! is_resource($filename)) {
+            throw new \Exception('first argument must be a string or stream resource');
+        } else {
+            $handle = $filename;
+        }
+
+        $file = $this->insertFile($record, $options);
+        $length = $this->insertChunksFromFile($handle, $file);
+
+        // Add length and MD5 if they were not present before
+        $update = [];
+        if (! isset($record['length'])) {
+            $update['length'] = $length;
+        }
+        if (! isset($record['md5'])) {
+            $update['md5'] = $this->getMd5ForFile($file['_id']);
+        }
+
+        if (count($update)) {
+            $this->update(['_id' => $file['_id']], ['$set' => $update]);
+        }
+
+        return $file['_id'];
+    }
 
     /**
      * Saves an uploaded file directly from a POST to the database
+     *
      * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
      * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
      * @param array $metadata An array of extra fields for the uploaded file.
      * @return mixed Returns the _id of the uploaded file.
+     * @throws MongoGridFSException
      */
-    public function storeUpload($name, array $metadata = array()) {}
+    public function storeUpload($name, array $metadata = [])
+    {
+        if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
+            throw new MongoGridFSException("Could not find uploaded file $name");
+        }
+        if (! isset($_FILES[$name]['tmp_name'])) {
+            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
+        }
+
+        $uploadedFile = $_FILES[$name];
+        $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
+        $uploadedFile['name'] = (array) $uploadedFile['name'];
 
+        if (count($uploadedFile['tmp_name']) > 1) {
+            foreach ($uploadedFile['tmp_name'] as $key => $file) {
+                $metadata['filename'] = $uploadedFile['name'][$key];
+                $this->storeFile($file, $metadata);
+            }
+
+            return null;
+        } else {
+            $metadata += ['filename' => array_pop($uploadedFile['name'])];
+            return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
+        }
+    }
 
     /**
-     * Retrieve a file from the database
-     * @link http://www.php.net/manual/en/mongogridfs.get.php
-     * @param mixed $id _id of the file to find.
-     * @return MongoGridFSFile|null Returns the file, if found, or NULL.
+     * Creates the index on the chunks collection
      */
-    public function __get($id) {}
+    private function createChunksIndex()
+    {
+        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
+    }
 
     /**
-     * Stores a file in the database
-     * @link http://php.net/manual/en/mongogridfs.put.php
-     * @param string $filename The name of the file
-     * @param array $extra Other metadata to add to the file saved
-     * @return mixed Returns the _id of the saved object
+     * Inserts a single chunk into the database
+     *
+     * @param mixed $fileId
+     * @param string $data
+     * @param int $chunkNumber
+     * @return array|bool
+     */
+    private function insertChunk($fileId, $data, $chunkNumber)
+    {
+        $chunk = [
+            'files_id' => $fileId,
+            'n' => $chunkNumber,
+            'data' => new MongoBinData($data),
+        ];
+        return $this->chunks->insert($chunk);
+    }
+
+    /**
+     * Splits a string into chunks and writes them to the database
+     *
+     * @param string $bytes
+     * @param array $record
+     */
+    private function insertChunksFromBytes($bytes, $record)
+    {
+        $chunkSize = $record['chunkSize'];
+        $fileId = $record['_id'];
+        $i = 0;
+
+        $chunks = str_split($bytes, $chunkSize);
+        foreach ($chunks as $chunk) {
+            $this->insertChunk($fileId, $chunk, $i++);
+        }
+    }
+
+    /**
+     * Reads chunks from a file and writes them to the database
+     *
+     * @param resource $handle
+     * @param array $record
+     * @return int Returns the number of bytes written to the database
+     */
+    private function insertChunksFromFile($handle, $record)
+    {
+        $written = 0;
+        $offset = 0;
+        $i = 0;
+
+        $fileId = $record['_id'];
+        $chunkSize = $record['chunkSize'];
+
+        rewind($handle);
+        while (! feof($handle)) {
+            $data = stream_get_contents($handle, $chunkSize);
+            $this->insertChunk($fileId, $data, $i++);
+            $written += strlen($data);
+            $offset += $chunkSize;
+        }
+
+        return $written;
+    }
+
+    /**
+     * Writes a file record to the database
+     *
+     * @param $record
+     * @param array $options
+     * @return array
      */
-    public function put($filename, array $extra = array()) {}
+    private function insertFile($record, array $options = [])
+    {
+        $record += [
+            '_id' => new MongoId(),
+            'uploadDate' => new MongoDate(),
+            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
+        ];
+
+        $this->insert($record, $options);
+
+        return $record;
+    }
 
+    /**
+     * Returns the MD5 string for a file previously stored to the database
+     *
+     * @param $id
+     * @return string
+     */
+    private function getMd5ForFile($id)
+    {
+        $result = $this->db->command(['filemd5' => $id, 'root' => $this->prefix]);
+        return $result['md5'];
+    }
 }

+ 21 - 13
lib/Mongo/MongoGridFSCursor.php

@@ -13,7 +13,8 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-class MongoGridFSCursor extends MongoCursor implements Traversable, Iterator {
+class MongoGridFSCursor extends MongoCursor
+{
     /**
      * @static
      * @var $slaveOkay
@@ -28,35 +29,42 @@ class MongoGridFSCursor extends MongoCursor implements Traversable, Iterator {
 
     /**
      * Create a new cursor
+     *
      * @link http://php.net/manual/en/mongogridfscursor.construct.php
      * @param MongoGridFS $gridfs Related GridFS collection
-     * @param resource $connection Database connection
+     * @param MongoClient $connection Database connection
      * @param string $ns Full name of database and collection
      * @param array $query Database query
      * @param array $fields Fields to return
      * @return MongoGridFSCursor Returns the new cursor
      */
-    public function __construct($gridfs, $connection, $ns, $query, $fields) {}
-
-    /**
-     * Return the next file to which this cursor points, and advance the cursor
-     * @link http://php.net/manual/en/mongogridfscursor.getnext.php
-     * @return MongoGridFSFile Returns the next file
-     */
-    public function getNext() {}
+    public function __construct(MongoGridFS $gridfs, MongoClient $connection, $ns, array $query = array(), array $fields = array())
+    {
+        $this->gridfs = $gridfs;
+        parent::__construct($connection, $ns, $query, $fields);
+    }
 
     /**
      * Returns the current file
+     *
      * @link http://php.net/manual/en/mongogridfscursor.current.php
      * @return MongoGridFSFile The current file
      */
-    public function current() {}
+    public function current()
+    {
+        $file = parent::current();
+        return ($file !== null) ? new MongoGridFSFile($this->gridfs, $file) : null;
+    }
 
     /**
      * Returns the current result's filename
+     *
      * @link http://php.net/manual/en/mongogridfscursor.key.php
      * @return string The current results filename
      */
-    public function key() {}
-
+    public function key()
+    {
+        $file = $this->current();
+        return ($file !== null) ? $file->getFilename() : null;
+    }
 }

+ 72 - 8
lib/Mongo/MongoGridFSFile.php

@@ -13,10 +13,11 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-class MongoGridFSFile {
+class MongoGridFSFile
+{
     /**
      * @link http://php.net/manual/en/class.mongogridfsfile.php#mongogridfsfile.props.file
-     * @var $file
+     * @var array
      */
     public $file;
 
@@ -28,25 +29,36 @@ class MongoGridFSFile {
 
     /**
      * @link http://php.net/manual/en/mongogridfsfile.construct.php
+     *
      * @param MongoGridFS $gridfs The parent MongoGridFS instance
      * @param array $file A file from the database
      * @return MongoGridFSFile Returns a new MongoGridFSFile
      */
-    public function __construct($gridfs, array $file) {}
+    public function __construct(MongoGridFS $gridfs, array $file)
+    {
+        $this->gridfs = $gridfs;
+        $this->file = $file;
+    }
 
     /**
      * Returns this file's filename
      * @link http://php.net/manual/en/mongogridfsfile.getfilename.php
      * @return string Returns the filename
      */
-    public function getFilename() {}
+    public function getFilename()
+    {
+        return isset($this->file['filename']) ? $this->file['filename'] : null;
+    }
 
     /**
      * Returns this file's size
      * @link http://php.net/manual/en/mongogridfsfile.getsize.php
      * @return int Returns this file's size
      */
-    public function getSize() {}
+    public function getSize()
+    {
+        return $this->file['length'];
+    }
 
     /**
      * Writes this file to the filesystem
@@ -54,14 +66,40 @@ class MongoGridFSFile {
      * @param string $filename The location to which to write the file (path+filename+extension). If none is given, the stored filename will be used.
      * @return int Returns the number of bytes written
      */
-    public function write($filename = null) {}
+    public function write($filename = null)
+    {
+        if ($filename === null) {
+            $filename = $this->getFilename();
+        }
+        if (empty($filename)) {
+            $filename = 'file';
+        }
+
+        if (! $handle = fopen($filename, 'w')) {
+            trigger_error(E_ERROR, 'Can not open the destination file');
+            return 0;
+        }
+
+        $written = $this->copyToResource($handle);
+        fclose($handle);
+
+        return $written;
+    }
 
     /**
      * This will load the file into memory. If the file is bigger than your memory, this will cause problems!
      * @link http://php.net/manual/en/mongogridfsfile.getbytes.php
      * @return string Returns a string of the bytes in the file
      */
-    public function getBytes() {}
+    public function getBytes()
+    {
+        $result = '';
+        foreach ($this->getChunks() as $chunk) {
+            $result .= $chunk['data']->bin;
+        }
+
+        return $result;
+    }
 
     /**
      * This method returns a stream resource that can be used to read the stored file with all file functions in PHP.
@@ -71,5 +109,31 @@ class MongoGridFSFile {
      * @link http://php.net/manual/en/mongogridfsfile.getresource.php
      * @return resource Returns a resource that can be used to read the file with
      */
-    public function getResource() {}
+    public function getResource()
+    {
+        $handle = fopen('php://temp', 'w+');
+        $this->copyToResource($handle);
+        rewind($handle);
+
+        return $handle;
+    }
+
+    private function copyToResource($handle)
+    {
+        $written = 0;
+        foreach ($this->getChunks() as $chunk) {
+            $written += fwrite($handle, $chunk['data']->bin);
+        }
+
+        return $written;
+    }
+
+    private function getChunks()
+    {
+        return $chunks = $this->gridfs->chunks->find(
+            ['files_id' => $this->file['_id']],
+            ['data' => 1],
+            ['n' => 1]
+        );
+    }
 }

+ 12 - 0
tests/Alcaeus/MongoDbAdapter/MongoCollectionTest.php

@@ -339,6 +339,18 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('bar', 'foo', $object);
     }
 
+    public function testRemoveOne()
+    {
+        $id = '54203e08d51d4a1f868b456e';
+        $collection = $this->getCollection();
+
+        $collection->insert(['_id' => new \MongoId($id), 'foo' => 'bar']);
+        $collection->remove(['_id' => new \MongoId($id)]);
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(0, $newCollection->count());
+    }
+
     public function testSaveUpdate()
     {
         $id = '54203e08d51d4a1f868b456e';

+ 28 - 0
tests/Alcaeus/MongoDbAdapter/MongoGridFSCursorTest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoGridFSCursorTest extends TestCase
+{
+    public function testCursorItems()
+    {
+        $gridfs = $this->getGridFS();
+        $gridfs->storeBytes('foo', ['filename' => 'foo.txt']);
+        $gridfs->storeBytes('bar', ['filename' => 'bar.txt']);
+
+        $cursor = $gridfs->find(['filename' => 'foo.txt']);
+        $this->assertCount(1, $cursor);
+        foreach ($cursor as $key => $value) {
+            $this->assertSame('foo.txt', $key);
+            $this->assertInstanceOf('MongoGridFSFile', $value);
+            $this->assertSame('foo', $value->getBytes());
+
+            $this->assertArraySubset([
+                'filename' => 'foo.txt',
+                'chunkSize' => \MongoGridFS::DEFAULT_CHUNK_SIZE,
+                'length' => 3,
+                'md5' => 'acbd18db4cc2f85cedef654fccc4a4d8'
+            ], $value->file);
+        }
+    }
+}

+ 123 - 0
tests/Alcaeus/MongoDbAdapter/MongoGridFSFileTest.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoGridFSFileTest extends TestCase
+{
+    public function testFileProperty()
+    {
+        $file = $this->getFile();
+        $this->assertArrayHasKey('_id', $file->file);
+        $this->assertArraySubset(
+            [
+                'length' => 666,
+                'filename' => 'file',
+                'md5' => 'md5',
+            ],
+            $file->file
+        );
+    }
+
+    public function testGetFilename()
+    {
+        $file = $this->getFile();
+        $this->assertSame('file', $file->getFilename());
+    }
+
+    public function testGetSize()
+    {
+        $file = $this->getFile();
+        $this->assertSame(666, $file->getSize());
+    }
+
+    public function testWrite()
+    {
+        $id = $this->prepareFile();
+        $filename = '/tmp/test-mongo-grid-fs-file';
+        @unlink($filename);
+        $file = $this->getFile(['_id' => $id, 'length' => 4, 'filename' => $filename]);
+
+        $file->write();
+
+        $this->assertTrue(file_exists($filename));
+        $this->assertSame('e2fc714c4727ee9395f324cd2e7f331f', md5_file($filename));
+        unlink($filename);
+    }
+
+    public function testWriteSpecifyFilename()
+    {
+        $id = $this->prepareFile();
+        $filename = '/tmp/test-mongo-grid-fs-file';
+        @unlink($filename);
+        $file = $this->getFile(['_id' => $id, 'length' => 4]);
+
+        $file->write($filename);
+
+        $this->assertTrue(file_exists($filename));
+        $this->assertSame('e2fc714c4727ee9395f324cd2e7f331f', md5_file($filename));
+        unlink($filename);
+    }
+
+    public function testGetBytes()
+    {
+        $id = $this->prepareFile();
+        $file = $this->getFile(['_id' => $id, 'length' => 4]);
+
+        $result = $file->getBytes();
+
+        $this->assertSame('abcd', $result);
+    }
+
+    public function testGetResource()
+    {
+        $id = $this->prepareFile();
+        $file = $this->getFile(['_id' => $id, 'length' => 4]);
+
+        $result = $file->getResource();
+
+        $this->assertTrue(is_resource($result));
+        $this->assertSame('abcd', stream_get_contents($result));
+    }
+
+    /**
+     * @var \MongoGridFSFile
+     */
+    protected function getFile($extra = [])
+    {
+        $file = [
+            '_id' => new \MongoID(),
+            'length' => 666,
+            'filename' => 'file',
+            'md5' => 'md5',
+        ];
+        $file = array_merge($file, $extra);
+        return new \MongoGridFSFile($this->getGridFS(), $file);
+    }
+
+    /**
+     * @var \MongoID
+     */
+    protected function prepareFile($data = 'abcd', $extra = [])
+    {
+        $collection = $this->getGridFS();
+
+        // to make sure we have multiple chunks
+        $extra += ['chunkSize' => 2];
+
+        return $collection->storeBytes($data, $extra);
+    }
+
+    /**
+     * @param string $name
+     * @param \MongoDB|null $database
+     * @return \MongoGridFS
+     */
+    protected function getGridFS($name = 'testfs', \MongoDB $database = null)
+    {
+        if ($database === null) {
+            $database = $this->getDatabase();
+        }
+
+        return new \MongoGridFS($database, $name);
+    }
+}

+ 320 - 0
tests/Alcaeus/MongoDbAdapter/MongoGridFSTest.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoGridFSTest extends TestCase
+{
+    public function testChunkProperty()
+    {
+        $collection = $this->getGridFS();
+        $this->assertInstanceOf('MongoCollection', $collection->chunks);
+        $this->assertSame('mongo-php-adapter.fs.chunks', (string) $collection->chunks);
+    }
+
+    public function testCustomCollectionName()
+    {
+        $collection = $this->getGridFS('foofs');
+        $this->assertSame('mongo-php-adapter.foofs.files', (string) $collection);
+        $this->assertInstanceOf('MongoCollection', $collection->chunks);
+        $this->assertSame('mongo-php-adapter.foofs.chunks', (string) $collection->chunks);
+    }
+
+    public function testDrop()
+    {
+        $collection = $this->getGridFS();
+
+        $collection->insert(['foo' => 'bar']);
+        $collection->chunks->insert(['foo' => 'bar']);
+
+        $collection->drop();
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(0, $newCollection->count());
+        $this->assertSame(0, $newChunksCollection->count());
+    }
+
+    public function testFindReturnsGridFSCursor()
+    {
+        $this->prepareData();
+        $collection = $this->getGridFS();
+
+        $this->assertInstanceOf('MongoGridFSCursor', $collection->find());
+    }
+
+    public function testStoringData()
+    {
+        $collection = $this->getGridFS();
+
+        $id = $collection->storeBytes(
+            'abcd',
+            [
+                'foo' => 'bar',
+                'chunkSize' => 2,
+            ]
+        );
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(1, $newCollection->count());
+        $this->assertSame(2, $newChunksCollection->count());
+
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record);
+        $this->assertSame((string) $id, (string) $record->_id);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+        $this->assertObjectHasAttribute('length', $record);
+        $this->assertAttributeSame(4, 'length', $record);
+        $this->assertObjectHasAttribute('chunkSize', $record);
+        $this->assertAttributeSame(2, 'chunkSize', $record);
+        $this->assertObjectHasAttribute('md5', $record);
+        $this->assertAttributeSame('e2fc714c4727ee9395f324cd2e7f331f', 'md5', $record);
+
+        $chunksCursor = $newChunksCollection->find([], ['sort' => ['n' => 1]]);
+        $chunks = iterator_to_array($chunksCursor);
+        $firstChunk = $chunks[0];
+        $this->assertNotNull($firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk);
+        $this->assertSame((string) $id, (string) $firstChunk->files_id);
+        $this->assertAttributeSame(0, 'n', $firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk);
+        $this->assertSame('ab', (string) $firstChunk->data->getData());
+
+        $secondChunck = $chunks[1];
+        $this->assertNotNull($secondChunck);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $secondChunck);
+        $this->assertSame((string) $id, (string) $secondChunck->files_id);
+        $this->assertAttributeSame(1, 'n', $secondChunck);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $secondChunck);
+        $this->assertSame('cd', (string) $secondChunck->data->getData());
+    }
+
+    public function testIndexesCreation()
+    {
+        $collection = $this->getGridFS();
+
+        $id = $collection->storeBytes(
+            'abcd',
+            [
+                'foo' => 'bar',
+                'chunkSize' => 2,
+            ]
+        );
+
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $indexes = iterator_to_array($newChunksCollection->listIndexes());
+        $this->assertCount(2, $indexes);
+        $index = $indexes[1];
+        $this->assertSame(['files_id' => 1, 'n' => 1], $index->getKey());
+        $this->assertTrue($index->isUnique());
+    }
+
+
+    public function testDelete()
+    {
+        $collection = $this->getGridFS();
+        $id = $this->prepareFile();
+
+        $collection->delete($id);
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(0, $newCollection->count());
+        $this->assertSame(0, $newChunksCollection->count());
+    }
+
+    public function testRemove()
+    {
+        $collection = $this->getGridFS();
+        $this->prepareFile('data', ['foo' => 'bar']);
+        $this->prepareFile('data', ['foo' => 'bar']);
+
+        $collection->remove(['foo' => 'bar']);
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(0, $newCollection->count());
+        $this->assertSame(0, $newChunksCollection->count());
+    }
+
+    public function testStoreFile()
+    {
+        $collection = $this->getGridFS();
+
+        $id = $collection->storeFile(__FILE__, ['chunkSize' => 100, 'foo' => 'bar']);
+
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(1, $newCollection->count());
+
+        $filename = __FILE__;
+        $md5 = md5_file($filename);
+        $size = filesize($filename);
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record);
+        $this->assertSame((string) $id, (string) $record->_id);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+        $this->assertObjectHasAttribute('length', $record);
+        $this->assertAttributeSame($size, 'length', $record);
+        $this->assertObjectHasAttribute('chunkSize', $record);
+        $this->assertAttributeSame(100, 'chunkSize', $record);
+        $this->assertObjectHasAttribute('md5', $record);
+        $this->assertAttributeSame($md5, 'md5', $record);
+        $this->assertObjectHasAttribute('filename', $record);
+        $this->assertAttributeSame($filename, 'filename', $record);
+
+        $numberOfChunks = (int)ceil($size / 100);
+        $this->assertSame($numberOfChunks, $newChunksCollection->count());
+        $expectedContent = substr(file_get_contents(__FILE__), 0, 100);
+
+        $firstChunk = $newChunksCollection->findOne([], ['sort' => ['n' => 1]]);
+        $this->assertNotNull($firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk);
+        $this->assertSame((string) $id, (string) $firstChunk->files_id);
+        $this->assertAttributeSame(0, 'n', $firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk);
+        $this->assertSame($expectedContent, (string) $firstChunk->data->getData());
+    }
+
+    public function testStoreFileResource()
+    {
+        $collection = $this->getGridFS();
+
+        $id = $collection->storeFile(
+            fopen(__FILE__, 'r'),
+            ['chunkSize' => 100, 'foo' => 'bar', 'filename' => 'test.php']
+        );
+
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(1, $newCollection->count());
+
+        $md5 = md5_file(__FILE__);
+        $size = filesize(__FILE__);
+        $filename = basename(__FILE__);
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record);
+        $this->assertSame((string) $id, (string) $record->_id);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+        $this->assertObjectHasAttribute('length', $record);
+        $this->assertAttributeSame($size, 'length', $record);
+        $this->assertObjectHasAttribute('chunkSize', $record);
+        $this->assertAttributeSame(100, 'chunkSize', $record);
+        $this->assertObjectHasAttribute('md5', $record);
+        $this->assertAttributeSame($md5, 'md5', $record);
+        $this->assertObjectHasAttribute('filename', $record);
+        $this->assertAttributeSame('test.php', 'filename', $record);
+
+        $numberOfChunks = (int)ceil($size / 100);
+        $this->assertSame($numberOfChunks, $newChunksCollection->count());
+        $expectedContent = substr(file_get_contents(__FILE__), 0, 100);
+
+        $firstChunk = $newChunksCollection->findOne([], ['sort' => ['n' => 1]]);
+        $this->assertNotNull($firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', 'files_id', $firstChunk);
+        $this->assertSame((string) $id, (string) $firstChunk->files_id);
+        $this->assertAttributeSame(0, 'n', $firstChunk);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\Binary', 'data', $firstChunk);
+        $this->assertSame($expectedContent, (string) $firstChunk->data->getData());
+    }
+
+    public function testStoreUpload()
+    {
+        $collection = $this->getGridFS();
+
+        $_FILES['foo'] = [
+            'name' => 'test.php',
+            'error' => UPLOAD_ERR_OK,
+            'tmp_name' => __FILE__,
+        ];
+
+        $id = $collection->storeUpload(
+            'foo',
+            ['chunkSize' => 100, 'foo' => 'bar']
+        );
+
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(1, $newCollection->count());
+
+        $md5 = md5_file(__FILE__);
+        $size = filesize(__FILE__);
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record);
+        $this->assertSame((string) $id, (string) $record->_id);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+        $this->assertObjectHasAttribute('length', $record);
+        $this->assertAttributeSame($size, 'length', $record);
+        $this->assertObjectHasAttribute('chunkSize', $record);
+        $this->assertAttributeSame(100, 'chunkSize', $record);
+        $this->assertObjectHasAttribute('md5', $record);
+        $this->assertAttributeSame($md5, 'md5', $record);
+        $this->assertObjectHasAttribute('filename', $record);
+        $this->assertAttributeSame('test.php', 'filename', $record);
+
+        $numberOfChunks = (int)ceil($size / 100);
+        $this->assertSame($numberOfChunks, $newChunksCollection->count());
+    }
+
+    public function testFindOneReturnsFile()
+    {
+        $collection = $this->getGridFS();
+        $this->prepareFile();
+
+        $result = $collection->findOne();
+
+        $this->assertInstanceOf('MongoGridFSFile', $result);
+    }
+
+    public function testPut()
+    {
+        $collection = $this->getGridFS();
+
+        $id = $collection->put(__FILE__, ['chunkSize' => 100, 'foo' => 'bar']);
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
+        $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
+        $this->assertSame(1, $newCollection->count());
+
+        $size = filesize(__FILE__);
+        $numberOfChunks = (int)ceil($size / 100);
+        $this->assertSame($numberOfChunks, $newChunksCollection->count());
+    }
+
+    /**
+     * @var \MongoID
+     */
+    protected function prepareFile($data = 'abcd', $extra = [])
+    {
+        $collection = $this->getGridFS();
+
+        // to make sure we have multiple chunks
+        $extra += ['chunkSize' => 2];
+
+        return $collection->storeBytes($data, $extra);
+    }
+
+    /**
+     * @return \MongoCollection
+     */
+    protected function prepareData()
+    {
+        $collection = $this->getGridFS();
+
+        $collection->insert(['foo' => 'bar']);
+        $collection->insert(['foo' => 'bar']);
+        $collection->insert(['foo' => 'foo']);
+        return $collection;
+    }
+}

+ 14 - 0
tests/Alcaeus/MongoDbAdapter/TestCase.php

@@ -62,4 +62,18 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
 
         return $database->selectCollection($name);
     }
+
+    /**
+     * @param string $prefix
+     * @param \MongoDB|null $database
+     * @return \MongoGridFS
+     */
+    protected function getGridFS($prefix = 'fs', \MongoDB $database = null)
+    {
+        if ($database === null) {
+            $database = $this->getDatabase();
+        }
+
+        return $database->getGridFS($prefix);
+    }
 }