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