MongoGridFS.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. */
  15. if (class_exists('MongoGridFS', false)) {
  16. return;
  17. }
  18. class MongoGridFS extends MongoCollection
  19. {
  20. const ASCENDING = 1;
  21. const DESCENDING = -1;
  22. /**
  23. * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunks
  24. * @var $chunks MongoCollection
  25. */
  26. public $chunks;
  27. /**
  28. * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.filesname
  29. * @var $filesName string
  30. */
  31. protected $filesName;
  32. /**
  33. * @link http://php.net/manual/en/class.mongogridfs.php#mongogridfs.props.chunksname
  34. * @var $chunksName string
  35. */
  36. protected $chunksName;
  37. /**
  38. * @var MongoDB
  39. */
  40. private $database;
  41. private $prefix;
  42. private $defaultChunkSize = 261120;
  43. /**
  44. * Files as stored across two collections, the first containing file meta
  45. * information, the second containing chunks of the actual file. By default,
  46. * fs.files and fs.chunks are the collection names used.
  47. *
  48. * @link http://php.net/manual/en/mongogridfs.construct.php
  49. * @param MongoDB $db Database
  50. * @param string $prefix [optional] <p>Optional collection name prefix.</p>
  51. * @param mixed $chunks [optional]
  52. * @throws \Exception
  53. */
  54. public function __construct(MongoDB $db, $prefix = "fs", $chunks = null)
  55. {
  56. if ($chunks) {
  57. trigger_error("The 'chunks' argument is deprecated and ignored", E_USER_DEPRECATED);
  58. }
  59. if (empty($prefix)) {
  60. throw new \Exception('MongoGridFS::__construct(): invalid prefix');
  61. }
  62. $this->database = $db;
  63. $this->prefix = (string) $prefix;
  64. $this->filesName = $prefix . '.files';
  65. $this->chunksName = $prefix . '.chunks';
  66. $this->chunks = $db->selectCollection($this->chunksName);
  67. parent::__construct($db, $this->filesName);
  68. }
  69. /**
  70. * Delete a file from the database
  71. *
  72. * @link http://php.net/manual/en/mongogridfs.delete.php
  73. * @param mixed $id _id of the file to remove
  74. * @return boolean Returns true if the remove was successfully sent to the database.
  75. */
  76. public function delete($id)
  77. {
  78. $this->createChunksIndex();
  79. $this->chunks->remove(['files_id' => $id], ['justOne' => false]);
  80. return parent::remove(['_id' => $id]);
  81. }
  82. /**
  83. * Drops the files and chunks collections
  84. * @link http://php.net/manual/en/mongogridfs.drop.php
  85. * @return array The database response
  86. */
  87. public function drop()
  88. {
  89. $this->chunks->drop();
  90. return parent::drop();
  91. }
  92. /**
  93. * @link http://php.net/manual/en/mongogridfs.find.php
  94. * @param array $query The query
  95. * @param array $fields Fields to return
  96. * @param array $options Options for the find command
  97. * @return MongoGridFSCursor A MongoGridFSCursor
  98. */
  99. public function find(array $query = [], array $fields = [])
  100. {
  101. $cursor = new MongoGridFSCursor($this, $this->db->getConnection(), (string) $this, $query, $fields);
  102. $cursor->setReadPreference($this->getReadPreference());
  103. return $cursor;
  104. }
  105. /**
  106. * Returns a single file matching the criteria
  107. *
  108. * @link http://www.php.net/manual/en/mongogridfs.findone.php
  109. * @param mixed $query The fields for which to search or a filename to search for.
  110. * @param array $fields Fields of the results to return.
  111. * @param array $options Options for the find command
  112. * @return MongoGridFSFile|null
  113. */
  114. public function findOne($query = [], array $fields = [], array $options = [])
  115. {
  116. if (! is_array($query)) {
  117. $query = ['filename' => (string) $query];
  118. }
  119. $items = iterator_to_array($this->find($query, $fields)->limit(1));
  120. return count($items) ? current($items) : null;
  121. }
  122. /**
  123. * Retrieve a file from the database
  124. *
  125. * @link http://www.php.net/manual/en/mongogridfs.get.php
  126. * @param mixed $id _id of the file to find.
  127. * @return MongoGridFSFile|null
  128. */
  129. public function get($id)
  130. {
  131. return $this->findOne(['_id' => $id]);
  132. }
  133. /**
  134. * Stores a file in the database
  135. *
  136. * @link http://php.net/manual/en/mongogridfs.put.php
  137. * @param string $filename The name of the file
  138. * @param array $extra Other metadata to add to the file saved
  139. * @param array $options An array of options for the insert operations executed against the chunks and files collections.
  140. * @return mixed Returns the _id of the saved object
  141. */
  142. public function put($filename, array $extra = [], array $options = [])
  143. {
  144. return $this->storeFile($filename, $extra, $options);
  145. }
  146. /**
  147. * Removes files from the collections
  148. *
  149. * @link http://www.php.net/manual/en/mongogridfs.remove.php
  150. * @param array $criteria Description of records to remove.
  151. * @param array $options Options for remove.
  152. * @throws MongoCursorException
  153. * @return boolean
  154. */
  155. public function remove(array $criteria = [], array $options = [])
  156. {
  157. $this->createChunksIndex();
  158. $matchingFiles = parent::find($criteria, ['_id' => 1]);
  159. $ids = [];
  160. foreach ($matchingFiles as $file) {
  161. $ids[] = $file['_id'];
  162. }
  163. $this->chunks->remove(['files_id' => ['$in' => $ids]], ['justOne' => false] + $options);
  164. return parent::remove(['_id' => ['$in' => $ids]], ['justOne' => false] + $options);
  165. }
  166. /**
  167. * Chunkifies and stores bytes in the database
  168. * @link http://php.net/manual/en/mongogridfs.storebytes.php
  169. * @param string $bytes A string of bytes to store
  170. * @param array $extra Other metadata to add to the file saved
  171. * @param array $options Options for the store. "safe": Check that this store succeeded
  172. * @return mixed The _id of the object saved
  173. */
  174. public function storeBytes($bytes, array $extra = [], array $options = [])
  175. {
  176. $this->createChunksIndex();
  177. $record = $extra + [
  178. 'length' => mb_strlen($bytes, '8bit'),
  179. 'md5' => md5($bytes),
  180. ];
  181. try {
  182. $file = $this->insertFile($record, $options);
  183. } catch (MongoException $e) {
  184. throw new MongoGridFSException('Could not store file: '. $e->getMessage(), 0, $e);
  185. }
  186. try {
  187. $this->insertChunksFromBytes($bytes, $file);
  188. } catch (MongoException $e) {
  189. $this->delete($file['_id']);
  190. throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
  191. }
  192. return $file['_id'];
  193. }
  194. /**
  195. * Stores a file in the database
  196. *
  197. * @link http://php.net/manual/en/mongogridfs.storefile.php
  198. * @param string $filename The name of the file
  199. * @param array $extra Other metadata to add to the file saved
  200. * @param array $options Options for the store. "safe": Check that this store succeeded
  201. * @return mixed Returns the _id of the saved object
  202. * @throws MongoGridFSException
  203. * @throws Exception
  204. */
  205. public function storeFile($filename, array $extra = [], array $options = [])
  206. {
  207. $this->createChunksIndex();
  208. $record = $extra;
  209. if (is_string($filename)) {
  210. $record += [
  211. 'md5' => md5_file($filename),
  212. 'length' => filesize($filename),
  213. 'filename' => $filename,
  214. ];
  215. $handle = fopen($filename, 'r');
  216. if (! $handle) {
  217. throw new MongoGridFSException('could not open file: ' . $filename);
  218. }
  219. } elseif (! is_resource($filename)) {
  220. throw new \Exception('first argument must be a string or stream resource');
  221. } else {
  222. $handle = $filename;
  223. }
  224. $md5 = null;
  225. try {
  226. $file = $this->insertFile($record, $options);
  227. } catch (MongoException $e) {
  228. throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
  229. }
  230. try {
  231. $length = $this->insertChunksFromFile($handle, $file, $md5);
  232. } catch (MongoException $e) {
  233. $this->delete($file['_id']);
  234. throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
  235. }
  236. // Add length and MD5 if they were not present before
  237. $update = [];
  238. if (! isset($record['length'])) {
  239. $update['length'] = $length;
  240. }
  241. if (! isset($record['md5'])) {
  242. try {
  243. $update['md5'] = $md5;
  244. } catch (MongoException $e) {
  245. throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
  246. }
  247. }
  248. if (count($update)) {
  249. try {
  250. $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
  251. if (! $this->isOKResult($result)) {
  252. throw new MongoGridFSException('Could not store file');
  253. }
  254. } catch (MongoException $e) {
  255. $this->delete($file['_id']);
  256. throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
  257. }
  258. }
  259. return $file['_id'];
  260. }
  261. /**
  262. * Saves an uploaded file directly from a POST to the database
  263. *
  264. * @link http://www.php.net/manual/en/mongogridfs.storeupload.php
  265. * @param string $name The name attribute of the uploaded file, from <input type="file" name="something"/>.
  266. * @param array $metadata An array of extra fields for the uploaded file.
  267. * @return mixed Returns the _id of the uploaded file.
  268. * @throws MongoGridFSException
  269. */
  270. public function storeUpload($name, array $metadata = [])
  271. {
  272. if (! isset($_FILES[$name]) || $_FILES[$name]['error'] !== UPLOAD_ERR_OK) {
  273. throw new MongoGridFSException("Could not find uploaded file $name");
  274. }
  275. if (! isset($_FILES[$name]['tmp_name'])) {
  276. throw new MongoGridFSException("Couldn't find tmp_name in the \$_FILES array. Are you sure the upload worked?");
  277. }
  278. $uploadedFile = $_FILES[$name];
  279. $uploadedFile['tmp_name'] = (array) $uploadedFile['tmp_name'];
  280. $uploadedFile['name'] = (array) $uploadedFile['name'];
  281. if (count($uploadedFile['tmp_name']) > 1) {
  282. foreach ($uploadedFile['tmp_name'] as $key => $file) {
  283. $metadata['filename'] = $uploadedFile['name'][$key];
  284. $this->storeFile($file, $metadata);
  285. }
  286. return null;
  287. } else {
  288. $metadata += ['filename' => array_pop($uploadedFile['name'])];
  289. return $this->storeFile(array_pop($uploadedFile['tmp_name']), $metadata);
  290. }
  291. }
  292. /**
  293. * Creates the index on the chunks collection
  294. */
  295. private function createChunksIndex()
  296. {
  297. try {
  298. $this->chunks->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
  299. } catch (MongoDuplicateKeyException $e) {
  300. }
  301. }
  302. /**
  303. * Inserts a single chunk into the database
  304. *
  305. * @param mixed $fileId
  306. * @param string $data
  307. * @param int $chunkNumber
  308. * @return array|bool
  309. */
  310. private function insertChunk($fileId, $data, $chunkNumber)
  311. {
  312. $chunk = [
  313. 'files_id' => $fileId,
  314. 'n' => $chunkNumber,
  315. 'data' => new MongoBinData($data),
  316. ];
  317. $result = $this->chunks->insert($chunk);
  318. if (! $this->isOKResult($result)) {
  319. throw new \MongoException('error inserting chunk');
  320. }
  321. return $result;
  322. }
  323. /**
  324. * Splits a string into chunks and writes them to the database
  325. *
  326. * @param string $bytes
  327. * @param array $record
  328. */
  329. private function insertChunksFromBytes($bytes, $record)
  330. {
  331. $chunkSize = $record['chunkSize'];
  332. $fileId = $record['_id'];
  333. $i = 0;
  334. $chunks = str_split($bytes, $chunkSize);
  335. foreach ($chunks as $chunk) {
  336. $this->insertChunk($fileId, $chunk, $i++);
  337. }
  338. }
  339. /**
  340. * Reads chunks from a file and writes them to the database
  341. *
  342. * @param resource $handle
  343. * @param array $record
  344. * @param string $md5
  345. * @return int Returns the number of bytes written to the database
  346. */
  347. private function insertChunksFromFile($handle, $record, &$md5)
  348. {
  349. $written = 0;
  350. $offset = 0;
  351. $i = 0;
  352. $fileId = $record['_id'];
  353. $chunkSize = $record['chunkSize'];
  354. $hash = hash_init('md5');
  355. rewind($handle);
  356. while (! feof($handle)) {
  357. $data = stream_get_contents($handle, $chunkSize);
  358. hash_update($hash, $data);
  359. $this->insertChunk($fileId, $data, $i++);
  360. $written += strlen($data);
  361. $offset += $chunkSize;
  362. }
  363. $md5 = hash_final($hash);
  364. return $written;
  365. }
  366. /**
  367. * Writes a file record to the database
  368. *
  369. * @param $record
  370. * @param array $options
  371. * @return array
  372. */
  373. private function insertFile($record, array $options = [])
  374. {
  375. $record += [
  376. '_id' => new MongoId(),
  377. 'uploadDate' => new MongoDate(),
  378. 'chunkSize' => $this->defaultChunkSize,
  379. ];
  380. $result = $this->insert($record, $options);
  381. if (! $this->isOKResult($result)) {
  382. throw new \MongoException('error inserting file');
  383. }
  384. return $record;
  385. }
  386. private function isOKResult($result)
  387. {
  388. return (is_array($result) && $result['ok'] == 1.0) ||
  389. (is_bool($result) && $result);
  390. }
  391. /**
  392. * @return array
  393. */
  394. public function __sleep()
  395. {
  396. return ['chunks', 'chunksName', 'database', 'defaultChunkSize', 'filesName', 'prefix'] + parent::__sleep();
  397. }
  398. }