Procházet zdrojové kódy

Refactor MongoGridFS class

Andreas Braun před 10 roky
rodič
revize
01ac77d9c5

+ 216 - 158
lib/Mongo/MongoGridFS.php

@@ -41,9 +41,9 @@ class MongoGridFS extends MongoCollection
     /**
      * @var MongoDB
      */
-    protected $database;
+    private $database;
 
-    protected $ensureIndexes = false;
+    private $prefix;
 
     /**
      * Files as stored across two collections, the first containing file meta
@@ -67,6 +67,7 @@ class MongoGridFS extends MongoCollection
         }
 
         $this->database = $db;
+        $this->prefix = $prefix;
         $this->filesName = $prefix . '.files';
         $this->chunksName = $prefix . '.chunks';
 
@@ -76,6 +77,21 @@ class MongoGridFS extends MongoCollection
     }
 
     /**
+     * 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
@@ -83,259 +99,301 @@ class MongoGridFS extends MongoCollection
     public function drop()
     {
         $this->chunks->drop();
-        parent::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 = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
         $cursor->setReadPreference($this->getReadPreference());
 
         return $cursor;
     }
 
     /**
-     * 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
+     * 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 storeFile($filename, array $extra = array(), array $options = array())
+    public function findOne(array $query = [], array $fields = [], array $options = [])
     {
-        if (is_string($filename)) {
-            $md5 = md5_file($filename);
-            $shortName = basename($filename);
-            $filename = fopen($filename, 'r');
+        if (is_string($query)) {
+            $query = ['filename' => $query];
         }
-        if (! is_resource($filename)) {
-            throw new \InvalidArgumentException();
-        }
-        $length = fstat($filename)['size'];
-        $extra['chunkSize'] = isset($extra['chunkSize']) ? $extra['chunkSize']: self::DEFAULT_CHUNK_SIZE;
-        $extra['_id'] = isset($extra['_id']) ?: new MongoId();
-        $extra['length'] = $length;
-        $extra['md5'] = isset($md5) ? $md5 : $this->calculateMD5($filename);
-        $extra['filename'] = isset($extra['filename']) ? $extra['filename'] : $shortName;
-
-        $fileDocument = $this->insertFile($extra);
-        $this->insertChunksFromFile($filename, $fileDocument);
 
-        return $fileDocument['_id'];
+        $items = iterator_to_array($this->find($query, $fields)->limit(1));
+        return current($items);
     }
 
     /**
-     * Chunkifies and stores bytes in the database
-     * @link http://php.net/manual/en/mongogridfs.storebytes.php
-     * @param string $bytes A string of bytes to store
-     * @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 The _id of the object saved
+     * 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 storeBytes($bytes, array $extra = array(), array $options = array())
+    public function get($id)
     {
-        $length = mb_strlen($bytes, '8bit');
-        $extra['chunkSize'] = isset($extra['chunkSize']) ? $extra['chunkSize'] : self::DEFAULT_CHUNK_SIZE;
-        $extra['_id'] = isset($extra['_id']) ?: new MongoId();
-        $extra['length'] = $length;
-        $extra['md5'] = md5($bytes);
-
-        $file = $this->insertFile($extra);
-        $this->insertChunksFromBytes($bytes, $file);
-
-        return $file['_id'];
+        return $this->findOne(['_id' => $id]);
     }
 
     /**
-     * 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
+     * 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
+     * @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 findOne(array $query = [], array $fields = [], array $options = [])
+    public function put($filename, array $extra = [], array $options = [])
     {
-        $file = parent::findOne($query, $fields);
-        if (! $file) {
-            return;
-        }
-        return new MongoGridFSFile($this, $file);
+        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. Valid options are: "safe"- Check that the remove succeeded.
+     * @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]);
-        return parent::remove($criteria, ['justOne' => false] + $options);
+        $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
+        return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
     }
 
     /**
-     * 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.
+     * Chunkifies and stores bytes in the database
+     * @link http://php.net/manual/en/mongogridfs.storebytes.php
+     * @param string $bytes A string of bytes to store
+     * @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 The _id of the object saved
      */
-    public function delete($id)
+    public function storeBytes($bytes, array $extra = [], array $options = [])
+    {
+        $this->createChunksIndex();
+
+        $record = $extra + [
+            'length' => mb_strlen($bytes, '8bit'),
+            'md5' => md5($bytes),
+        ];
+
+        $file = $this->insertFile($record, $options);
+        $this->insertChunksFromBytes($bytes, $file);
+
+        return $file['_id'];
+    }
+
+    /**
+     * 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 storeFile($filename, array $extra = [], array $options = [])
     {
-        if (is_string($id)) {
-            $id = new MongoId($id);
+        $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;
         }
-        if (! $id instanceof MongoId) {
-            return false;
+
+        $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;
         }
-        $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
-        return parent::remove(['_id' => $id]);
+        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 \InvalidArgumentException();
+            throw new MongoGridFSException("Could not find uploaded file $name");
         }
-        $metadata += ['filename' => $_FILES[$name]['name']];
-        return $this->storeFile($_FILES[$name]['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.
-     */
-    public function __get($id)
-    {
-        if (is_string($id)) {
-            $id = new MongoId($id);
+        if (! isset($_FILES[$name]['tmp_name'])) {
+            throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
         }
-        if (! $id instanceof MongoId) {
-            return false;
+
+        $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);
         }
-        return $this->findOne(['_id' => $id]);
     }
 
     /**
-     * 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
+     * Creates the index on the chunks collection
      */
-    public function put($filename, array $extra = array())
+    private function createChunksIndex()
     {
-        return $this->storeFile($filename, $extra);
+        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
     }
 
-    private function ensureIndexes()
+    /**
+     * 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)
     {
-        if ($this->ensureIndexes) {
-            return;
-        }
-        $this->ensureFilesIndex();
-        $this->ensureChunksIndex();
-        $this->ensuredIndexes = true;
+        $chunk = [
+            'files_id' => $fileId,
+            'n' => $chunkNumber,
+            'data' => new MongoBinData($data),
+        ];
+        return $this->chunks->insert($chunk);
     }
 
-    private function ensureChunksIndex()
+    /**
+     * Splits a string into chunks and writes them to the database
+     *
+     * @param string $bytes
+     * @param array $record
+     */
+    private function insertChunksFromBytes($bytes, $record)
     {
-        foreach ($this->chunks->getIndexInfo() as $index) {
-            if (isset($index['unique']) && $index['unique'] && $index['key'] === ['files_id' => 1, 'n' => 1]) {
-                return;
-            }
-        }
-        $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
-    }
+        $chunkSize = $record['chunkSize'];
+        $fileId = $record['_id'];
+        $i = 0;
 
-    private function ensureFilesIndex()
-    {
-        foreach ($this->getIndexInfo() as $index) {
-            if ($index['key'] === ['filename' => 1, 'uploadDate' => 1]) {
-                return;
-            }
+        $chunks = str_split($bytes, $chunkSize);
+        foreach ($chunks as $chunk) {
+            $this->insertChunk($fileId, $chunk, $i++);
         }
-        $this->createIndex(['filename' => 1, 'uploadDate' => 1]);
     }
 
-    private function insertChunksFromFile($file, $fileInfo)
+    /**
+     * 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)
     {
-        $length = $fileInfo['length'];
-        $chunkSize = $fileInfo['chunkSize'];
-        $fileId = $fileInfo['_id'];
+        $written = 0;
         $offset = 0;
         $i = 0;
 
-        rewind($file);
+        $fileId = $record['_id'];
+        $chunkSize = $record['chunkSize'];
 
-        while ($offset < $length) {
-            $data = stream_get_contents($file, $chunkSize);
+        rewind($handle);
+        while (! feof($handle)) {
+            $data = stream_get_contents($handle, $chunkSize);
             $this->insertChunk($fileId, $data, $i++);
+            $written += strlen($data);
             $offset += $chunkSize;
         }
-    }
-
-    private function calculateMD5($file)
-    {
-        // XXX: this could be really a bad idea with big files...
-        rewind($file);
-        $data = stream_get_contents($file);
 
-        return md5($data);
+        return $written;
     }
 
-    private function insertChunksFromBytes($bytes, $fileInfo)
+    /**
+     * Writes a file record to the database
+     *
+     * @param $record
+     * @param array $options
+     * @return array
+     */
+    private function insertFile($record, array $options = [])
     {
-        $length = $fileInfo['length'];
-        $chunkSize = $fileInfo['chunkSize'];
-        $fileId = $fileInfo['_id'];
-        $i = 0;
+        $record += [
+            '_id' => new MongoId(),
+            'uploadDate' => new MongoDate(),
+            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
+        ];
 
-        $chunks = str_split($bytes, $chunkSize);
-        foreach ($chunks as $chunk) {
-            $this->insertChunk($fileId, $chunk, $i++);
-        }
-    }
+        $this->insert($record, $options);
 
-    private function insertChunk($id, $data, $chunkNumber)
-    {
-        $chunk = [
-            'files_id' => $id,
-            'n' => $chunkNumber,
-            'data' => new MongoBinData($data),
-        ];
-        return $this->chunks->insert($chunk);
+        return $record;
     }
 
-    private function insertFile($metadata)
+    /**
+     * Returns the MD5 string for a file previously stored to the database
+     *
+     * @param $id
+     * @return string
+     */
+    private function getMd5ForFile($id)
     {
-        $this->ensureIndexes();
-        $metadata['uploadDate'] = new MongoDate();
-        $this->insert($metadata);
-        return $metadata;
+        $result = $this->db->command(['filemd5' => $id, 'root' => $this->prefix]);
+        return $result['md5'];
     }
-
 }

+ 5 - 13
tests/Alcaeus/MongoDbAdapter/MongoGridFSTest.php

@@ -103,13 +103,6 @@ class MongoGridFSTest extends TestCase
             ]
         );
 
-        $newCollection = $this->getCheckDatabase()->selectCollection('fs.files');
-
-        $indexes = iterator_to_array($newCollection->listIndexes());
-        $this->assertCount(2, $indexes);
-        $index = $indexes[1];
-        $this->assertSame(['filename' => 1, 'uploadDate' => 1], $index->getKey());
-
         $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
         $indexes = iterator_to_array($newChunksCollection->listIndexes());
         $this->assertCount(2, $indexes);
@@ -157,9 +150,9 @@ class MongoGridFSTest extends TestCase
         $newChunksCollection = $this->getCheckDatabase()->selectCollection('fs.chunks');
         $this->assertSame(1, $newCollection->count());
 
-        $md5 = md5_file(__FILE__);
-        $size = filesize(__FILE__);
-        $filename = basename(__FILE__);
+        $filename = __FILE__;
+        $md5 = md5_file($filename);
+        $size = filesize($filename);
         $record = $newCollection->findOne();
         $this->assertNotNull($record);
         $this->assertAttributeInstanceOf('MongoDB\BSON\ObjectID', '_id', $record);
@@ -249,13 +242,12 @@ class MongoGridFSTest extends TestCase
         );
 
 
-        $newCollection = $this->getCheckDatabase()->selectCollection('testfs.files');
-        $newChunksCollection = $this->getCheckDatabase()->selectCollection('testfs.chunks');
+        $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);