Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401]
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 18 namespace MongoDB\GridFS; 19 20 use MongoDB\Collection; 21 use MongoDB\Driver\Cursor; 22 use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; 23 use MongoDB\Driver\Manager; 24 use MongoDB\Driver\ReadConcern; 25 use MongoDB\Driver\ReadPreference; 26 use MongoDB\Driver\WriteConcern; 27 use MongoDB\Exception\InvalidArgumentException; 28 use MongoDB\Exception\UnsupportedException; 29 use MongoDB\GridFS\Exception\CorruptFileException; 30 use MongoDB\GridFS\Exception\FileNotFoundException; 31 use MongoDB\Model\BSONArray; 32 use MongoDB\Model\BSONDocument; 33 use MongoDB\Operation\Find; 34 use stdClass; 35 use function array_intersect_key; 36 use function fopen; 37 use function get_resource_type; 38 use function in_array; 39 use function is_array; 40 use function is_bool; 41 use function is_integer; 42 use function is_object; 43 use function is_resource; 44 use function is_string; 45 use function method_exists; 46 use function MongoDB\apply_type_map_to_document; 47 use function MongoDB\BSON\fromPHP; 48 use function MongoDB\BSON\toJSON; 49 use function property_exists; 50 use function sprintf; 51 use function stream_context_create; 52 use function stream_copy_to_stream; 53 use function stream_get_meta_data; 54 use function stream_get_wrappers; 55 use function urlencode; 56 57 /** 58 * Bucket provides a public API for interacting with the GridFS files and chunks 59 * collections. 60 * 61 * @api 62 */ 63 class Bucket 64 { 65 /** @var string */ 66 private static $defaultBucketName = 'fs'; 67 68 /** @var integer */ 69 private static $defaultChunkSizeBytes = 261120; 70 71 /** @var array */ 72 private static $defaultTypeMap = [ 73 'array' => BSONArray::class, 74 'document' => BSONDocument::class, 75 'root' => BSONDocument::class, 76 ]; 77 78 /** @var string */ 79 private static $streamWrapperProtocol = 'gridfs'; 80 81 /** @var CollectionWrapper */ 82 private $collectionWrapper; 83 84 /** @var string */ 85 private $databaseName; 86 87 /** @var Manager */ 88 private $manager; 89 90 /** @var string */ 91 private $bucketName; 92 93 /** @var boolean */ 94 private $disableMD5; 95 96 /** @var integer */ 97 private $chunkSizeBytes; 98 99 /** @var ReadConcern */ 100 private $readConcern; 101 102 /** @var ReadPreference */ 103 private $readPreference; 104 105 /** @var array */ 106 private $typeMap; 107 108 /** @var WriteConcern */ 109 private $writeConcern; 110 111 /** 112 * Constructs a GridFS bucket. 113 * 114 * Supported options: 115 * 116 * * bucketName (string): The bucket name, which will be used as a prefix 117 * for the files and chunks collections. Defaults to "fs". 118 * 119 * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to 120 * 261120 (i.e. 255 KiB). 121 * 122 * * disableMD5 (boolean): When true, no MD5 sum will be generated for 123 * each stored file. Defaults to "false". 124 * 125 * * readConcern (MongoDB\Driver\ReadConcern): Read concern. 126 * 127 * * readPreference (MongoDB\Driver\ReadPreference): Read preference. 128 * 129 * * typeMap (array): Default type map for cursors and BSON documents. 130 * 131 * * writeConcern (MongoDB\Driver\WriteConcern): Write concern. 132 * 133 * @param Manager $manager Manager instance from the driver 134 * @param string $databaseName Database name 135 * @param array $options Bucket options 136 * @throws InvalidArgumentException for parameter/option parsing errors 137 */ 138 public function __construct(Manager $manager, $databaseName, array $options = []) 139 { 140 $options += [ 141 'bucketName' => self::$defaultBucketName, 142 'chunkSizeBytes' => self::$defaultChunkSizeBytes, 143 'disableMD5' => false, 144 ]; 145 146 if (isset($options['bucketName']) && ! is_string($options['bucketName'])) { 147 throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string'); 148 } 149 150 if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) { 151 throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer'); 152 } 153 154 if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) { 155 throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes'])); 156 } 157 158 if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) { 159 throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean'); 160 } 161 162 if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) { 163 throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class); 164 } 165 166 if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) { 167 throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class); 168 } 169 170 if (isset($options['typeMap']) && ! is_array($options['typeMap'])) { 171 throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array'); 172 } 173 174 if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) { 175 throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); 176 } 177 178 $this->manager = $manager; 179 $this->databaseName = (string) $databaseName; 180 $this->bucketName = $options['bucketName']; 181 $this->chunkSizeBytes = $options['chunkSizeBytes']; 182 $this->disableMD5 = $options['disableMD5']; 183 $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern(); 184 $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference(); 185 $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap; 186 $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern(); 187 188 $collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]); 189 190 $this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions); 191 $this->registerStreamWrapper(); 192 } 193 194 /** 195 * Return internal properties for debugging purposes. 196 * 197 * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo 198 * @return array 199 */ 200 public function __debugInfo() 201 { 202 return [ 203 'bucketName' => $this->bucketName, 204 'databaseName' => $this->databaseName, 205 'manager' => $this->manager, 206 'chunkSizeBytes' => $this->chunkSizeBytes, 207 'readConcern' => $this->readConcern, 208 'readPreference' => $this->readPreference, 209 'typeMap' => $this->typeMap, 210 'writeConcern' => $this->writeConcern, 211 ]; 212 } 213 214 /** 215 * Delete a file from the GridFS bucket. 216 * 217 * If the files collection document is not found, this method will still 218 * attempt to delete orphaned chunks. 219 * 220 * @param mixed $id File ID 221 * @throws FileNotFoundException if no file could be selected 222 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 223 */ 224 public function delete($id) 225 { 226 $file = $this->collectionWrapper->findFileById($id); 227 $this->collectionWrapper->deleteFileAndChunksById($id); 228 229 if ($file === null) { 230 throw FileNotFoundException::byId($id, $this->getFilesNamespace()); 231 } 232 } 233 234 /** 235 * Writes the contents of a GridFS file to a writable stream. 236 * 237 * @param mixed $id File ID 238 * @param resource $destination Writable Stream 239 * @throws FileNotFoundException if no file could be selected 240 * @throws InvalidArgumentException if $destination is not a stream 241 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 242 */ 243 public function downloadToStream($id, $destination) 244 { 245 if (! is_resource($destination) || get_resource_type($destination) != "stream") { 246 throw InvalidArgumentException::invalidType('$destination', $destination, 'resource'); 247 } 248 249 stream_copy_to_stream($this->openDownloadStream($id), $destination); 250 } 251 252 /** 253 * Writes the contents of a GridFS file, which is selected by name and 254 * revision, to a writable stream. 255 * 256 * Supported options: 257 * 258 * * revision (integer): Which revision (i.e. documents with the same 259 * filename and different uploadDate) of the file to retrieve. Defaults 260 * to -1 (i.e. the most recent revision). 261 * 262 * Revision numbers are defined as follows: 263 * 264 * * 0 = the original stored file 265 * * 1 = the first revision 266 * * 2 = the second revision 267 * * etc… 268 * * -2 = the second most recent revision 269 * * -1 = the most recent revision 270 * 271 * @param string $filename Filename 272 * @param resource $destination Writable Stream 273 * @param array $options Download options 274 * @throws FileNotFoundException if no file could be selected 275 * @throws InvalidArgumentException if $destination is not a stream 276 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 277 */ 278 public function downloadToStreamByName($filename, $destination, array $options = []) 279 { 280 if (! is_resource($destination) || get_resource_type($destination) != "stream") { 281 throw InvalidArgumentException::invalidType('$destination', $destination, 'resource'); 282 } 283 284 stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination); 285 } 286 287 /** 288 * Drops the files and chunks collections associated with this GridFS 289 * bucket. 290 * 291 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 292 */ 293 public function drop() 294 { 295 $this->collectionWrapper->dropCollections(); 296 } 297 298 /** 299 * Finds documents from the GridFS bucket's files collection matching the 300 * query. 301 * 302 * @see Find::__construct() for supported options 303 * @param array|object $filter Query by which to filter documents 304 * @param array $options Additional options 305 * @return Cursor 306 * @throws UnsupportedException if options are not supported by the selected server 307 * @throws InvalidArgumentException for parameter/option parsing errors 308 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 309 */ 310 public function find($filter = [], array $options = []) 311 { 312 return $this->collectionWrapper->findFiles($filter, $options); 313 } 314 315 /** 316 * Finds a single document from the GridFS bucket's files collection 317 * matching the query. 318 * 319 * @see FindOne::__construct() for supported options 320 * @param array|object $filter Query by which to filter documents 321 * @param array $options Additional options 322 * @return array|object|null 323 * @throws UnsupportedException if options are not supported by the selected server 324 * @throws InvalidArgumentException for parameter/option parsing errors 325 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 326 */ 327 public function findOne($filter = [], array $options = []) 328 { 329 return $this->collectionWrapper->findOneFile($filter, $options); 330 } 331 332 /** 333 * Return the bucket name. 334 * 335 * @return string 336 */ 337 public function getBucketName() 338 { 339 return $this->bucketName; 340 } 341 342 /** 343 * Return the chunks collection. 344 * 345 * @return Collection 346 */ 347 public function getChunksCollection() 348 { 349 return $this->collectionWrapper->getChunksCollection(); 350 } 351 352 /** 353 * Return the chunk size in bytes. 354 * 355 * @return integer 356 */ 357 public function getChunkSizeBytes() 358 { 359 return $this->chunkSizeBytes; 360 } 361 362 /** 363 * Return the database name. 364 * 365 * @return string 366 */ 367 public function getDatabaseName() 368 { 369 return $this->databaseName; 370 } 371 372 /** 373 * Gets the file document of the GridFS file associated with a stream. 374 * 375 * @param resource $stream GridFS stream 376 * @return array|object 377 * @throws InvalidArgumentException if $stream is not a GridFS stream 378 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 379 */ 380 public function getFileDocumentForStream($stream) 381 { 382 $file = $this->getRawFileDocumentForStream($stream); 383 384 // Filter the raw document through the specified type map 385 return apply_type_map_to_document($file, $this->typeMap); 386 } 387 388 /** 389 * Gets the file document's ID of the GridFS file associated with a stream. 390 * 391 * @param resource $stream GridFS stream 392 * @return mixed 393 * @throws CorruptFileException if the file "_id" field does not exist 394 * @throws InvalidArgumentException if $stream is not a GridFS stream 395 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 396 */ 397 public function getFileIdForStream($stream) 398 { 399 $file = $this->getRawFileDocumentForStream($stream); 400 401 /* Filter the raw document through the specified type map, but override 402 * the root type so we can reliably access the ID. 403 */ 404 $typeMap = ['root' => 'stdClass'] + $this->typeMap; 405 $file = apply_type_map_to_document($file, $typeMap); 406 407 if (! isset($file->_id) && ! property_exists($file, '_id')) { 408 throw new CorruptFileException('file._id does not exist'); 409 } 410 411 return $file->_id; 412 } 413 414 /** 415 * Return the files collection. 416 * 417 * @return Collection 418 */ 419 public function getFilesCollection() 420 { 421 return $this->collectionWrapper->getFilesCollection(); 422 } 423 424 /** 425 * Return the read concern for this GridFS bucket. 426 * 427 * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php 428 * @return ReadConcern 429 */ 430 public function getReadConcern() 431 { 432 return $this->readConcern; 433 } 434 435 /** 436 * Return the read preference for this GridFS bucket. 437 * 438 * @return ReadPreference 439 */ 440 public function getReadPreference() 441 { 442 return $this->readPreference; 443 } 444 445 /** 446 * Return the type map for this GridFS bucket. 447 * 448 * @return array 449 */ 450 public function getTypeMap() 451 { 452 return $this->typeMap; 453 } 454 455 /** 456 * Return the write concern for this GridFS bucket. 457 * 458 * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php 459 * @return WriteConcern 460 */ 461 public function getWriteConcern() 462 { 463 return $this->writeConcern; 464 } 465 466 /** 467 * Opens a readable stream for reading a GridFS file. 468 * 469 * @param mixed $id File ID 470 * @return resource 471 * @throws FileNotFoundException if no file could be selected 472 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 473 */ 474 public function openDownloadStream($id) 475 { 476 $file = $this->collectionWrapper->findFileById($id); 477 478 if ($file === null) { 479 throw FileNotFoundException::byId($id, $this->getFilesNamespace()); 480 } 481 482 return $this->openDownloadStreamByFile($file); 483 } 484 485 /** 486 * Opens a readable stream stream to read a GridFS file, which is selected 487 * by name and revision. 488 * 489 * Supported options: 490 * 491 * * revision (integer): Which revision (i.e. documents with the same 492 * filename and different uploadDate) of the file to retrieve. Defaults 493 * to -1 (i.e. the most recent revision). 494 * 495 * Revision numbers are defined as follows: 496 * 497 * * 0 = the original stored file 498 * * 1 = the first revision 499 * * 2 = the second revision 500 * * etc… 501 * * -2 = the second most recent revision 502 * * -1 = the most recent revision 503 * 504 * @param string $filename Filename 505 * @param array $options Download options 506 * @return resource 507 * @throws FileNotFoundException if no file could be selected 508 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 509 */ 510 public function openDownloadStreamByName($filename, array $options = []) 511 { 512 $options += ['revision' => -1]; 513 514 $file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']); 515 516 if ($file === null) { 517 throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace()); 518 } 519 520 return $this->openDownloadStreamByFile($file); 521 } 522 523 /** 524 * Opens a writable stream for writing a GridFS file. 525 * 526 * Supported options: 527 * 528 * * _id (mixed): File document identifier. Defaults to a new ObjectId. 529 * 530 * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the 531 * bucket's chunk size. 532 * 533 * * disableMD5 (boolean): When true, no MD5 sum will be generated for 534 * the stored file. Defaults to "false". 535 * 536 * * metadata (document): User data for the "metadata" field of the files 537 * collection document. 538 * 539 * @param string $filename Filename 540 * @param array $options Upload options 541 * @return resource 542 */ 543 public function openUploadStream($filename, array $options = []) 544 { 545 $options += ['chunkSizeBytes' => $this->chunkSizeBytes]; 546 547 $path = $this->createPathForUpload(); 548 $context = stream_context_create([ 549 self::$streamWrapperProtocol => [ 550 'collectionWrapper' => $this->collectionWrapper, 551 'filename' => $filename, 552 'options' => $options, 553 ], 554 ]); 555 556 return fopen($path, 'w', false, $context); 557 } 558 559 /** 560 * Renames the GridFS file with the specified ID. 561 * 562 * @param mixed $id File ID 563 * @param string $newFilename New filename 564 * @throws FileNotFoundException if no file could be selected 565 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 566 */ 567 public function rename($id, $newFilename) 568 { 569 $updateResult = $this->collectionWrapper->updateFilenameForId($id, $newFilename); 570 571 if ($updateResult->getModifiedCount() === 1) { 572 return; 573 } 574 575 /* If the update resulted in no modification, it's possible that the 576 * file did not exist, in which case we must raise an error. Checking 577 * the write result's matched count will be most efficient, but fall 578 * back to a findOne operation if necessary (i.e. legacy writes). 579 */ 580 $found = $updateResult->getMatchedCount() !== null 581 ? $updateResult->getMatchedCount() === 1 582 : $this->collectionWrapper->findFileById($id) !== null; 583 584 if (! $found) { 585 throw FileNotFoundException::byId($id, $this->getFilesNamespace()); 586 } 587 } 588 589 /** 590 * Writes the contents of a readable stream to a GridFS file. 591 * 592 * Supported options: 593 * 594 * * _id (mixed): File document identifier. Defaults to a new ObjectId. 595 * 596 * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the 597 * bucket's chunk size. 598 * 599 * * disableMD5 (boolean): When true, no MD5 sum will be generated for 600 * the stored file. Defaults to "false". 601 * 602 * * metadata (document): User data for the "metadata" field of the files 603 * collection document. 604 * 605 * @param string $filename Filename 606 * @param resource $source Readable stream 607 * @param array $options Stream options 608 * @return mixed ID of the newly created GridFS file 609 * @throws InvalidArgumentException if $source is not a GridFS stream 610 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 611 */ 612 public function uploadFromStream($filename, $source, array $options = []) 613 { 614 if (! is_resource($source) || get_resource_type($source) != "stream") { 615 throw InvalidArgumentException::invalidType('$source', $source, 'resource'); 616 } 617 618 $destination = $this->openUploadStream($filename, $options); 619 stream_copy_to_stream($source, $destination); 620 621 return $this->getFileIdForStream($destination); 622 } 623 624 /** 625 * Creates a path for an existing GridFS file. 626 * 627 * @param stdClass $file GridFS file document 628 * @return string 629 */ 630 private function createPathForFile(stdClass $file) 631 { 632 if (! is_object($file->_id) || method_exists($file->_id, '__toString')) { 633 $id = (string) $file->_id; 634 } else { 635 $id = toJSON(fromPHP(['_id' => $file->_id])); 636 } 637 638 return sprintf( 639 '%s://%s/%s.files/%s', 640 self::$streamWrapperProtocol, 641 urlencode($this->databaseName), 642 urlencode($this->bucketName), 643 urlencode($id) 644 ); 645 } 646 647 /** 648 * Creates a path for a new GridFS file, which does not yet have an ID. 649 * 650 * @return string 651 */ 652 private function createPathForUpload() 653 { 654 return sprintf( 655 '%s://%s/%s.files', 656 self::$streamWrapperProtocol, 657 urlencode($this->databaseName), 658 urlencode($this->bucketName) 659 ); 660 } 661 662 /** 663 * Returns the names of the files collection. 664 * 665 * @return string 666 */ 667 private function getFilesNamespace() 668 { 669 return sprintf('%s.%s.files', $this->databaseName, $this->bucketName); 670 } 671 672 /** 673 * Gets the file document of the GridFS file associated with a stream. 674 * 675 * This returns the raw document from the StreamWrapper, which does not 676 * respect the Bucket's type map. 677 * 678 * @param resource $stream GridFS stream 679 * @return stdClass 680 * @throws InvalidArgumentException 681 */ 682 private function getRawFileDocumentForStream($stream) 683 { 684 if (! is_resource($stream) || get_resource_type($stream) != "stream") { 685 throw InvalidArgumentException::invalidType('$stream', $stream, 'resource'); 686 } 687 688 $metadata = stream_get_meta_data($stream); 689 690 if (! isset($metadata['wrapper_data']) || ! $metadata['wrapper_data'] instanceof StreamWrapper) { 691 throw InvalidArgumentException::invalidType('$stream wrapper data', isset($metadata['wrapper_data']) ? $metadata['wrapper_data'] : null, StreamWrapper::class); 692 } 693 694 return $metadata['wrapper_data']->getFile(); 695 } 696 697 /** 698 * Opens a readable stream for the GridFS file. 699 * 700 * @param stdClass $file GridFS file document 701 * @return resource 702 */ 703 private function openDownloadStreamByFile(stdClass $file) 704 { 705 $path = $this->createPathForFile($file); 706 $context = stream_context_create([ 707 self::$streamWrapperProtocol => [ 708 'collectionWrapper' => $this->collectionWrapper, 709 'file' => $file, 710 ], 711 ]); 712 713 return fopen($path, 'r', false, $context); 714 } 715 716 /** 717 * Registers the GridFS stream wrapper if it is not already registered. 718 */ 719 private function registerStreamWrapper() 720 { 721 if (in_array(self::$streamWrapperProtocol, stream_get_wrappers())) { 722 return; 723 } 724 725 StreamWrapper::register(self::$streamWrapperProtocol); 726 } 727 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body