Skip to content

Commit 5be27d1

Browse files
stobrien89Sean O'Brien
andauthored
bugfix: prevent resources provided to ObjectUploader from being closed by Guzzle (#3296)
Co-authored-by: Sean O'Brien <obrien.sean.dev@gmail.com>
1 parent c99c436 commit 5be27d1

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"type": "bugfix",
4+
"category": "S3",
5+
"description": "Prevents resources provided to `ObjectUploader` from being closed by Guzzle."
6+
}
7+
]

src/S3/ObjectUploader.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use GuzzleHttp\Promise\PromiseInterface;
55
use GuzzleHttp\Promise\PromisorInterface;
66
use GuzzleHttp\Psr7;
7+
use GuzzleHttp\Psr7\FnStream;
78
use Psr\Http\Message\StreamInterface;
89

910
/**
@@ -57,7 +58,7 @@ public function __construct(
5758
$this->client = $client;
5859
$this->bucket = $bucket;
5960
$this->key = $key;
60-
$this->body = Psr7\Utils::streamFor($body);
61+
$this->body = $this->createStream($body);
6162
$this->acl = $acl;
6263
$this->options = $options + self::$defaults;
6364
// Handle "add_content_md5" option.
@@ -100,6 +101,33 @@ public function upload()
100101
return $this->promise()->wait();
101102
}
102103

104+
/**
105+
* Creates a stream from the provided body, ensuring that user-provided
106+
* PHP stream resources are not closed when the stream is destructed.
107+
*
108+
* @param mixed $body
109+
*
110+
* @return StreamInterface
111+
*/
112+
private function createStream($body): StreamInterface
113+
{
114+
$stream = Psr7\Utils::streamFor($body);
115+
116+
// When the user provides a raw PHP resource, we should not take
117+
// ownership of it (i.e., we should not fclose it on destruct).
118+
// Decorate with a no-op close to prevent the underlying Stream
119+
// from closing the user's resource handle.
120+
if (is_resource($body)) {
121+
return FnStream::decorate($stream, [
122+
'close' => function () use ($stream) {
123+
$stream->detach();
124+
},
125+
]);
126+
}
127+
128+
return $stream;
129+
}
130+
103131
/**
104132
* Determines if the body should be uploaded using PutObject or the
105133
* Multipart Upload System. It also modifies the passed-in $body as needed

tests/S3/ObjectUploaderTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,4 +381,70 @@ public function testAddContentMd5EmitsDeprecationNotice()
381381
restore_error_handler();
382382
}
383383
}
384+
385+
/**
386+
* Tests that a user-provided PHP resource handle is NOT closed by
387+
* ObjectUploader after the upload completes (PutObject path).
388+
*
389+
* Regression test for: https://github.com/aws/aws-sdk-php/issues/XXXX
390+
* The cyclic reference fix (PR #3290) caused Stream::__destruct() to fire
391+
* promptly, which closed user-provided resource handles.
392+
*/
393+
public function testUploadDoesNotCloseUserProvidedResourceHandle()
394+
{
395+
$client = $this->getTestClient('S3');
396+
$this->addMockResults($client, [new Result()]);
397+
398+
$fp = fopen('php://memory', 'r+');
399+
fwrite($fp, 'Hello, World!');
400+
fseek($fp, 0);
401+
402+
(new ObjectUploader($client, 'bucket', 'key', $fp))->upload();
403+
404+
$this->assertTrue(
405+
is_resource($fp),
406+
'User-provided resource handle should remain open after upload()'
407+
);
408+
409+
// Clean up
410+
fclose($fp);
411+
}
412+
413+
/**
414+
* Tests that a user-provided PHP resource handle is NOT closed by
415+
* ObjectUploader after the upload completes (multipart path).
416+
*/
417+
public function testMultipartUploadDoesNotCloseUserProvidedResourceHandle()
418+
{
419+
$client = $this->getTestClient('S3');
420+
$this->addMockResults($client, [
421+
new Result(['UploadId' => 'foo']),
422+
new Result(['ETag' => 'bar']),
423+
new Result(['ETag' => 'bar']),
424+
new Result(['Location' => 'https://bucket.s3.amazonaws.com/key']),
425+
]);
426+
427+
// Create a resource with content larger than the multipart threshold
428+
$fp = fopen('php://temp', 'r+');
429+
$data = str_repeat('x', 6 * self::MB);
430+
fwrite($fp, $data);
431+
fseek($fp, 0);
432+
433+
(new ObjectUploader(
434+
$client,
435+
'bucket',
436+
'key',
437+
$fp,
438+
'private',
439+
['mup_threshold' => 4 * self::MB]
440+
))->upload();
441+
442+
$this->assertTrue(
443+
is_resource($fp),
444+
'User-provided resource handle should remain open after multipart upload()'
445+
);
446+
447+
// Clean up
448+
fclose($fp);
449+
}
384450
}

0 commit comments

Comments
 (0)