PageRenderTime 39ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php

https://github.com/mackensen/moodle
PHP | 388 lines | 189 code | 45 blank | 154 comment | 13 complexity | e393a2172a63d3ebb12e3ce4e45bdcd9 MD5 | raw file
  1. <?php
  2. /*
  3. * Copyright 2016-2017 MongoDB, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. namespace MongoDB\GridFS;
  18. use ArrayIterator;
  19. use MongoDB\Collection;
  20. use MongoDB\Driver\Cursor;
  21. use MongoDB\Driver\Manager;
  22. use MongoDB\Driver\ReadPreference;
  23. use MongoDB\Exception\InvalidArgumentException;
  24. use MongoDB\UpdateResult;
  25. use MultipleIterator;
  26. use stdClass;
  27. use function abs;
  28. use function count;
  29. use function is_numeric;
  30. use function sprintf;
  31. /**
  32. * CollectionWrapper abstracts the GridFS files and chunks collections.
  33. *
  34. * @internal
  35. */
  36. class CollectionWrapper
  37. {
  38. /** @var string */
  39. private $bucketName;
  40. /** @var Collection */
  41. private $chunksCollection;
  42. /** @var string */
  43. private $databaseName;
  44. /** @var boolean */
  45. private $checkedIndexes = false;
  46. /** @var Collection */
  47. private $filesCollection;
  48. /**
  49. * Constructs a GridFS collection wrapper.
  50. *
  51. * @see Collection::__construct() for supported options
  52. * @param Manager $manager Manager instance from the driver
  53. * @param string $databaseName Database name
  54. * @param string $bucketName Bucket name
  55. * @param array $collectionOptions Collection options
  56. * @throws InvalidArgumentException
  57. */
  58. public function __construct(Manager $manager, $databaseName, $bucketName, array $collectionOptions = [])
  59. {
  60. $this->databaseName = (string) $databaseName;
  61. $this->bucketName = (string) $bucketName;
  62. $this->filesCollection = new Collection($manager, $databaseName, sprintf('%s.files', $bucketName), $collectionOptions);
  63. $this->chunksCollection = new Collection($manager, $databaseName, sprintf('%s.chunks', $bucketName), $collectionOptions);
  64. }
  65. /**
  66. * Deletes all GridFS chunks for a given file ID.
  67. *
  68. * @param mixed $id
  69. */
  70. public function deleteChunksByFilesId($id)
  71. {
  72. $this->chunksCollection->deleteMany(['files_id' => $id]);
  73. }
  74. /**
  75. * Deletes a GridFS file and related chunks by ID.
  76. *
  77. * @param mixed $id
  78. */
  79. public function deleteFileAndChunksById($id)
  80. {
  81. $this->filesCollection->deleteOne(['_id' => $id]);
  82. $this->chunksCollection->deleteMany(['files_id' => $id]);
  83. }
  84. /**
  85. * Drops the GridFS files and chunks collections.
  86. */
  87. public function dropCollections()
  88. {
  89. $this->filesCollection->drop(['typeMap' => []]);
  90. $this->chunksCollection->drop(['typeMap' => []]);
  91. }
  92. /**
  93. * Finds GridFS chunk documents for a given file ID and optional offset.
  94. *
  95. * @param mixed $id File ID
  96. * @param integer $fromChunk Starting chunk (inclusive)
  97. * @return Cursor
  98. */
  99. public function findChunksByFileId($id, $fromChunk = 0)
  100. {
  101. return $this->chunksCollection->find(
  102. [
  103. 'files_id' => $id,
  104. 'n' => ['$gte' => $fromChunk],
  105. ],
  106. [
  107. 'sort' => ['n' => 1],
  108. 'typeMap' => ['root' => 'stdClass'],
  109. ]
  110. );
  111. }
  112. /**
  113. * Finds a GridFS file document for a given filename and revision.
  114. *
  115. * Revision numbers are defined as follows:
  116. *
  117. * * 0 = the original stored file
  118. * * 1 = the first revision
  119. * * 2 = the second revision
  120. * * etc…
  121. * * -2 = the second most recent revision
  122. * * -1 = the most recent revision
  123. *
  124. * @see Bucket::downloadToStreamByName()
  125. * @see Bucket::openDownloadStreamByName()
  126. * @param string $filename
  127. * @param integer $revision
  128. * @return stdClass|null
  129. */
  130. public function findFileByFilenameAndRevision($filename, $revision)
  131. {
  132. $filename = (string) $filename;
  133. $revision = (integer) $revision;
  134. if ($revision < 0) {
  135. $skip = abs($revision) - 1;
  136. $sortOrder = -1;
  137. } else {
  138. $skip = $revision;
  139. $sortOrder = 1;
  140. }
  141. return $this->filesCollection->findOne(
  142. ['filename' => $filename],
  143. [
  144. 'skip' => $skip,
  145. 'sort' => ['uploadDate' => $sortOrder],
  146. 'typeMap' => ['root' => 'stdClass'],
  147. ]
  148. );
  149. }
  150. /**
  151. * Finds a GridFS file document for a given ID.
  152. *
  153. * @param mixed $id
  154. * @return stdClass|null
  155. */
  156. public function findFileById($id)
  157. {
  158. return $this->filesCollection->findOne(
  159. ['_id' => $id],
  160. ['typeMap' => ['root' => 'stdClass']]
  161. );
  162. }
  163. /**
  164. * Finds documents from the GridFS bucket's files collection.
  165. *
  166. * @see Find::__construct() for supported options
  167. * @param array|object $filter Query by which to filter documents
  168. * @param array $options Additional options
  169. * @return Cursor
  170. */
  171. public function findFiles($filter, array $options = [])
  172. {
  173. return $this->filesCollection->find($filter, $options);
  174. }
  175. /**
  176. * Finds a single document from the GridFS bucket's files collection.
  177. *
  178. * @param array|object $filter Query by which to filter documents
  179. * @param array $options Additional options
  180. * @return array|object|null
  181. */
  182. public function findOneFile($filter, array $options = [])
  183. {
  184. return $this->filesCollection->findOne($filter, $options);
  185. }
  186. /**
  187. * Return the bucket name.
  188. *
  189. * @return string
  190. */
  191. public function getBucketName()
  192. {
  193. return $this->bucketName;
  194. }
  195. /**
  196. * Return the chunks collection.
  197. *
  198. * @return Collection
  199. */
  200. public function getChunksCollection()
  201. {
  202. return $this->chunksCollection;
  203. }
  204. /**
  205. * Return the database name.
  206. *
  207. * @return string
  208. */
  209. public function getDatabaseName()
  210. {
  211. return $this->databaseName;
  212. }
  213. /**
  214. * Return the files collection.
  215. *
  216. * @return Collection
  217. */
  218. public function getFilesCollection()
  219. {
  220. return $this->filesCollection;
  221. }
  222. /**
  223. * Inserts a document into the chunks collection.
  224. *
  225. * @param array|object $chunk Chunk document
  226. */
  227. public function insertChunk($chunk)
  228. {
  229. if (! $this->checkedIndexes) {
  230. $this->ensureIndexes();
  231. }
  232. $this->chunksCollection->insertOne($chunk);
  233. }
  234. /**
  235. * Inserts a document into the files collection.
  236. *
  237. * The file document should be inserted after all chunks have been inserted.
  238. *
  239. * @param array|object $file File document
  240. */
  241. public function insertFile($file)
  242. {
  243. if (! $this->checkedIndexes) {
  244. $this->ensureIndexes();
  245. }
  246. $this->filesCollection->insertOne($file);
  247. }
  248. /**
  249. * Updates the filename field in the file document for a given ID.
  250. *
  251. * @param mixed $id
  252. * @param string $filename
  253. * @return UpdateResult
  254. */
  255. public function updateFilenameForId($id, $filename)
  256. {
  257. return $this->filesCollection->updateOne(
  258. ['_id' => $id],
  259. ['$set' => ['filename' => (string) $filename]]
  260. );
  261. }
  262. /**
  263. * Create an index on the chunks collection if it does not already exist.
  264. */
  265. private function ensureChunksIndex()
  266. {
  267. $expectedIndex = ['files_id' => 1, 'n' => 1];
  268. foreach ($this->chunksCollection->listIndexes() as $index) {
  269. if ($index->isUnique() && $this->indexKeysMatch($expectedIndex, $index->getKey())) {
  270. return;
  271. }
  272. }
  273. $this->chunksCollection->createIndex($expectedIndex, ['unique' => true]);
  274. }
  275. /**
  276. * Create an index on the files collection if it does not already exist.
  277. */
  278. private function ensureFilesIndex()
  279. {
  280. $expectedIndex = ['filename' => 1, 'uploadDate' => 1];
  281. foreach ($this->filesCollection->listIndexes() as $index) {
  282. if ($this->indexKeysMatch($expectedIndex, $index->getKey())) {
  283. return;
  284. }
  285. }
  286. $this->filesCollection->createIndex($expectedIndex);
  287. }
  288. /**
  289. * Ensure indexes on the files and chunks collections exist.
  290. *
  291. * This method is called once before the first write operation on a GridFS
  292. * bucket. Indexes are only be created if the files collection is empty.
  293. */
  294. private function ensureIndexes()
  295. {
  296. if ($this->checkedIndexes) {
  297. return;
  298. }
  299. $this->checkedIndexes = true;
  300. if (! $this->isFilesCollectionEmpty()) {
  301. return;
  302. }
  303. $this->ensureFilesIndex();
  304. $this->ensureChunksIndex();
  305. }
  306. private function indexKeysMatch(array $expectedKeys, array $actualKeys) : bool
  307. {
  308. if (count($expectedKeys) !== count($actualKeys)) {
  309. return false;
  310. }
  311. $iterator = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
  312. $iterator->attachIterator(new ArrayIterator($expectedKeys));
  313. $iterator->attachIterator(new ArrayIterator($actualKeys));
  314. foreach ($iterator as $key => $value) {
  315. list($expectedKey, $actualKey) = $key;
  316. list($expectedValue, $actualValue) = $value;
  317. if ($expectedKey !== $actualKey) {
  318. return false;
  319. }
  320. /* Since we don't expect special indexes (e.g. text), we mark any
  321. * index with a non-numeric definition as unequal. All others are
  322. * compared against their int value to avoid differences due to
  323. * some drivers using float values in the key specification. */
  324. if (! is_numeric($actualValue) || (int) $expectedValue !== (int) $actualValue) {
  325. return false;
  326. }
  327. }
  328. return true;
  329. }
  330. /**
  331. * Returns whether the files collection is empty.
  332. *
  333. * @return boolean
  334. */
  335. private function isFilesCollectionEmpty()
  336. {
  337. return null === $this->filesCollection->findOne([], [
  338. 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
  339. 'projection' => ['_id' => 1],
  340. 'typeMap' => [],
  341. ]);
  342. }
  343. }