Skip to content

Commit 07b994b

Browse files
authored
PHPLIB-425: GridFS methods should throw if stream copying fails (#776)
* PHPLIB-425: GridFS methods should throw if stream copying fails * Suppress error that occur during stream_copy_to_stream * Use more expressive exception messages * Print more usable information in stream exceptions * Add tests for stream exceptions * Fix phpcs violations * Fix wrong regex in tests * Skip test on PHP < 7.4 Apparently, the workaround of returning false to stream_read only works on PHP 7.4 but not on earlier versions * Avoid using new PHPUnit assertion
1 parent 5c77807 commit 07b994b

File tree

5 files changed

+160
-3
lines changed

5 files changed

+160
-3
lines changed

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
110110
<exclude-pattern>/src/GridFS/StreamWrapper</exclude-pattern>
111111
<exclude-pattern>/tests/DocumentationExamplesTest.php</exclude-pattern>
112+
<exclude-pattern>/tests/GridFS/UnusableStream.php</exclude-pattern>
112113
</rule>
113114

114115
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">

src/GridFS/Bucket.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use MongoDB\Exception\UnsupportedException;
2929
use MongoDB\GridFS\Exception\CorruptFileException;
3030
use MongoDB\GridFS\Exception\FileNotFoundException;
31+
use MongoDB\GridFS\Exception\StreamException;
3132
use MongoDB\Model\BSONArray;
3233
use MongoDB\Model\BSONDocument;
3334
use MongoDB\Operation\Find;
@@ -238,6 +239,7 @@ public function delete($id)
238239
* @param resource $destination Writable Stream
239240
* @throws FileNotFoundException if no file could be selected
240241
* @throws InvalidArgumentException if $destination is not a stream
242+
* @throws StreamException if the file could not be uploaded
241243
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
242244
*/
243245
public function downloadToStream($id, $destination)
@@ -246,7 +248,10 @@ public function downloadToStream($id, $destination)
246248
throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
247249
}
248250

249-
stream_copy_to_stream($this->openDownloadStream($id), $destination);
251+
$source = $this->openDownloadStream($id);
252+
if (@stream_copy_to_stream($source, $destination) === false) {
253+
throw StreamException::downloadFromIdFailed($id, $source, $destination);
254+
}
250255
}
251256

252257
/**
@@ -273,6 +278,7 @@ public function downloadToStream($id, $destination)
273278
* @param array $options Download options
274279
* @throws FileNotFoundException if no file could be selected
275280
* @throws InvalidArgumentException if $destination is not a stream
281+
* @throws StreamException if the file could not be uploaded
276282
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
277283
*/
278284
public function downloadToStreamByName($filename, $destination, array $options = [])
@@ -281,7 +287,10 @@ public function downloadToStreamByName($filename, $destination, array $options =
281287
throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
282288
}
283289

284-
stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination);
290+
$source = $this->openDownloadStreamByName($filename, $options);
291+
if (@stream_copy_to_stream($source, $destination) === false) {
292+
throw StreamException::downloadFromFilenameFailed($filename, $source, $destination);
293+
}
285294
}
286295

287296
/**
@@ -607,6 +616,7 @@ public function rename($id, $newFilename)
607616
* @param array $options Stream options
608617
* @return mixed ID of the newly created GridFS file
609618
* @throws InvalidArgumentException if $source is not a GridFS stream
619+
* @throws StreamException if the file could not be uploaded
610620
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
611621
*/
612622
public function uploadFromStream($filename, $source, array $options = [])
@@ -616,7 +626,11 @@ public function uploadFromStream($filename, $source, array $options = [])
616626
}
617627

618628
$destination = $this->openUploadStream($filename, $options);
619-
stream_copy_to_stream($source, $destination);
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+
}
620634

621635
return $this->getFileIdForStream($destination);
622636
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace MongoDB\GridFS\Exception;
4+
5+
use MongoDB\Exception\RuntimeException;
6+
use function MongoDB\BSON\fromPHP;
7+
use function MongoDB\BSON\toJSON;
8+
use function sprintf;
9+
use function stream_get_meta_data;
10+
11+
class StreamException extends RuntimeException
12+
{
13+
/**
14+
* @param resource $source
15+
* @param resource $destination
16+
*/
17+
public static function downloadFromFilenameFailed(string $filename, $source, $destination) : self
18+
{
19+
$sourceMetadata = stream_get_meta_data($source);
20+
$destinationMetadata = stream_get_meta_data($destination);
21+
22+
return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $filename));
23+
}
24+
25+
/**
26+
* @param mixed $id
27+
* @param resource $source
28+
* @param resource $destination
29+
*/
30+
public static function downloadFromIdFailed($id, $source, $destination) : self
31+
{
32+
$idString = toJSON(fromPHP(['_id' => $id]));
33+
$sourceMetadata = stream_get_meta_data($source);
34+
$destinationMetadata = stream_get_meta_data($destination);
35+
36+
return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS identifier: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $idString));
37+
}
38+
39+
/** @param resource $source */
40+
public static function uploadFailed(string $filename, $source, string $destinationUri) : self
41+
{
42+
$sourceMetadata = stream_get_meta_data($source);
43+
44+
return new static(sprintf('Uploading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationUri, $filename));
45+
}
46+
}

tests/GridFS/BucketFunctionalTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use MongoDB\Exception\InvalidArgumentException;
1111
use MongoDB\GridFS\Bucket;
1212
use MongoDB\GridFS\Exception\FileNotFoundException;
13+
use MongoDB\GridFS\Exception\StreamException;
1314
use MongoDB\Model\BSONDocument;
1415
use MongoDB\Model\IndexInfo;
1516
use MongoDB\Operation\ListCollections;
@@ -19,6 +20,7 @@
1920
use function call_user_func;
2021
use function current;
2122
use function fclose;
23+
use function fopen;
2224
use function fread;
2325
use function fwrite;
2426
use function hash_init;
@@ -29,6 +31,7 @@
2931
use function stream_get_contents;
3032
use function strlen;
3133
use function substr;
34+
use const PHP_VERSION_ID;
3235

3336
/**
3437
* Functional tests for the Bucket class.
@@ -708,6 +711,38 @@ public function testExistingIndexIsReused()
708711
$this->assertIndexNotExists($this->chunksCollection->getCollectionName(), 'files_id_1_n_1');
709712
}
710713

714+
public function testDownloadToStreamFails()
715+
{
716+
$this->bucket->uploadFromStream('filename', $this->createStream('foo'), ['_id' => ['foo' => 'bar']]);
717+
718+
$this->expectException(StreamException::class);
719+
$this->expectExceptionMessageRegExp('#^Downloading file from "gridfs://.*/.*/.*" to "php://temp" failed. GridFS identifier: "{ "_id" : { "foo" : "bar" } }"$#');
720+
$this->bucket->downloadToStream(['foo' => 'bar'], fopen('php://temp', 'r'));
721+
}
722+
723+
public function testDownloadToStreamByNameFails()
724+
{
725+
$this->bucket->uploadFromStream('filename', $this->createStream('foo'));
726+
727+
$this->expectException(StreamException::class);
728+
$this->expectExceptionMessageRegExp('#^Downloading file from "gridfs://.*/.*/.*" to "php://temp" failed. GridFS filename: "filename"$#');
729+
$this->bucket->downloadToStreamByName('filename', fopen('php://temp', 'r'));
730+
}
731+
732+
public function testUploadFromStreamFails()
733+
{
734+
if (PHP_VERSION_ID < 70400) {
735+
$this->markTestSkipped('Test only works on PHP 7.4 and newer');
736+
}
737+
738+
UnusableStream::register();
739+
$source = fopen('unusable://temp', 'w');
740+
741+
$this->expectException(StreamException::class);
742+
$this->expectExceptionMessageRegExp('#^Uploading file from "unusable://temp" to "gridfs://.*/.*/.*" failed. GridFS filename: "filename"$#');
743+
$this->bucket->uploadFromStream('filename', $source);
744+
}
745+
711746
/**
712747
* Asserts that a collection with the given name does not exist on the
713748
* server.

tests/GridFS/UnusableStream.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\GridFS;
4+
5+
use function in_array;
6+
use function stream_get_wrappers;
7+
use function stream_wrapper_register;
8+
use function stream_wrapper_unregister;
9+
use const SEEK_SET;
10+
use const STREAM_IS_URL;
11+
12+
final class UnusableStream
13+
{
14+
public static function register($protocol = 'unusable')
15+
{
16+
if (in_array($protocol, stream_get_wrappers())) {
17+
stream_wrapper_unregister($protocol);
18+
}
19+
20+
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
21+
}
22+
23+
public function stream_close()
24+
{
25+
}
26+
27+
public function stream_eof()
28+
{
29+
return true;
30+
}
31+
32+
public function stream_open($path, $mode, $options, &$openedPath)
33+
{
34+
return true;
35+
}
36+
37+
public function stream_read($length)
38+
{
39+
return false;
40+
}
41+
42+
public function stream_seek($offset, $whence = SEEK_SET)
43+
{
44+
return true;
45+
}
46+
47+
public function stream_stat()
48+
{
49+
return [];
50+
}
51+
52+
public function stream_tell()
53+
{
54+
return 0;
55+
}
56+
57+
public function stream_write($data)
58+
{
59+
return 0;
60+
}
61+
}

0 commit comments

Comments
 (0)