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