PluginProbe ʕ •ᴥ•ʔ
Transferito: WP Migration / trunk
Transferito: WP Migration vtrunk
trunk 11.4.0 12.0.0 13.1.0 14.0.0 14.0.11 14.0.7 14.1.0 14.1.1 14.1.2 14.1.3 14.1.4
transferito / vendor / aws / aws-sdk-php / src / S3 / StreamWrapper.php
transferito / vendor / aws / aws-sdk-php / src / S3 Last commit date
Crypto 11 months ago Exception 11 months ago RegionalEndpoint 11 months ago UseArnRegion 11 months ago AmbiguousSuccessParser.php 11 months ago ApplyChecksumMiddleware.php 11 months ago BatchDelete.php 11 months ago BucketEndpointArnMiddleware.php 11 months ago BucketEndpointMiddleware.php 11 months ago CalculatesChecksumTrait.php 11 months ago EndpointRegionHelperTrait.php 11 months ago GetBucketLocationParser.php 11 months ago MultipartCopy.php 11 months ago MultipartUploader.php 11 months ago MultipartUploadingTrait.php 11 months ago ObjectCopier.php 11 months ago ObjectUploader.php 11 months ago PermanentRedirectMiddleware.php 11 months ago PostObject.php 11 months ago PostObjectV4.php 11 months ago PutObjectUrlMiddleware.php 11 months ago RetryableMalformedResponseParser.php 11 months ago S3Client.php 11 months ago S3ClientInterface.php 11 months ago S3ClientTrait.php 11 months ago S3EndpointMiddleware.php 11 months ago S3MultiRegionClient.php 11 months ago S3UriParser.php 11 months ago SSECMiddleware.php 11 months ago StreamWrapper.php 11 months ago Transfer.php 11 months ago ValidateResponseChecksumParser.php 11 months ago
StreamWrapper.php
982 lines
1 <?php
2 namespace Aws\S3;
3
4 use Aws\CacheInterface;
5 use Aws\LruArrayCache;
6 use Aws\Result;
7 use Aws\S3\Exception\S3Exception;
8 use GuzzleHttp\Psr7;
9 use GuzzleHttp\Psr7\Stream;
10 use GuzzleHttp\Psr7\CachingStream;
11 use Psr\Http\Message\StreamInterface;
12
13 /**
14 * Amazon S3 stream wrapper to use "s3://<bucket>/<key>" files with PHP
15 * streams, supporting "r", "w", "a", "x".
16 *
17 * # Opening "r" (read only) streams:
18 *
19 * Read only streams are truly streaming by default and will not allow you to
20 * seek. This is because data read from the stream is not kept in memory or on
21 * the local filesystem. You can force a "r" stream to be seekable by setting
22 * the "seekable" stream context option true. This will allow true streaming of
23 * data from Amazon S3, but will maintain a buffer of previously read bytes in
24 * a 'php://temp' stream to allow seeking to previously read bytes from the
25 * stream.
26 *
27 * You may pass any GetObject parameters as 's3' stream context options. These
28 * options will affect how the data is downloaded from Amazon S3.
29 *
30 * # Opening "w" and "x" (write only) streams:
31 *
32 * Because Amazon S3 requires a Content-Length header, write only streams will
33 * maintain a 'php://temp' stream to buffer data written to the stream until
34 * the stream is flushed (usually by closing the stream with fclose).
35 *
36 * You may pass any PutObject parameters as 's3' stream context options. These
37 * options will affect how the data is uploaded to Amazon S3.
38 *
39 * When opening an "x" stream, the file must exist on Amazon S3 for the stream
40 * to open successfully.
41 *
42 * # Opening "a" (write only append) streams:
43 *
44 * Similar to "w" streams, opening append streams requires that the data be
45 * buffered in a "php://temp" stream. Append streams will attempt to download
46 * the contents of an object in Amazon S3, seek to the end of the object, then
47 * allow you to append to the contents of the object. The data will then be
48 * uploaded using a PutObject operation when the stream is flushed (usually
49 * with fclose).
50 *
51 * You may pass any GetObject and/or PutObject parameters as 's3' stream
52 * context options. These options will affect how the data is downloaded and
53 * uploaded from Amazon S3.
54 *
55 * Stream context options:
56 *
57 * - "seekable": Set to true to create a seekable "r" (read only) stream by
58 * using a php://temp stream buffer
59 * - For "unlink" only: Any option that can be passed to the DeleteObject
60 * operation
61 */
62 class StreamWrapper
63 {
64 /** @var resource|null Stream context (this is set by PHP) */
65 public $context;
66
67 /** @var StreamInterface Underlying stream resource */
68 private $body;
69
70 /** @var int Size of the body that is opened */
71 private $size;
72
73 /** @var array Hash of opened stream parameters */
74 private $params = [];
75
76 /** @var string Mode in which the stream was opened */
77 private $mode;
78
79 /** @var \Iterator Iterator used with opendir() related calls */
80 private $objectIterator;
81
82 /** @var string The bucket that was opened when opendir() was called */
83 private $openedBucket;
84
85 /** @var string The prefix of the bucket that was opened with opendir() */
86 private $openedBucketPrefix;
87
88 /** @var string Opened bucket path */
89 private $openedPath;
90
91 /** @var CacheInterface Cache for object and dir lookups */
92 private $cache;
93
94 /** @var string The opened protocol (e.g., "s3") */
95 private $protocol = 's3';
96
97 /** @var bool Keeps track of whether stream has been flushed since opening */
98 private $isFlushed = false;
99
100 /** @var bool Whether or not to use V2 bucket and object existence methods */
101 private static $useV2Existence = false;
102
103 /**
104 * Register the 's3://' stream wrapper
105 *
106 * @param S3ClientInterface $client Client to use with the stream wrapper
107 * @param string $protocol Protocol to register as.
108 * @param CacheInterface $cache Default cache for the protocol.
109 */
110 public static function register(
111 S3ClientInterface $client,
112 $protocol = 's3',
113 CacheInterface $cache = null,
114 $v2Existence = false
115 ) {
116 self::$useV2Existence = $v2Existence;
117 if (in_array($protocol, stream_get_wrappers())) {
118 stream_wrapper_unregister($protocol);
119 }
120
121 // Set the client passed in as the default stream context client
122 stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL);
123 $default = stream_context_get_options(stream_context_get_default());
124 $default[$protocol]['client'] = $client;
125
126 if ($cache) {
127 $default[$protocol]['cache'] = $cache;
128 } elseif (!isset($default[$protocol]['cache'])) {
129 // Set a default cache adapter.
130 $default[$protocol]['cache'] = new LruArrayCache();
131 }
132
133 stream_context_set_default($default);
134 }
135
136 public function stream_close()
137 {
138 if (!$this->isFlushed
139 && empty($this->body->getSize())
140 && $this->mode !== 'r'
141 ) {
142 $this->stream_flush();
143 }
144 $this->body = $this->cache = null;
145 }
146
147 public function stream_open($path, $mode, $options, &$opened_path)
148 {
149 $this->initProtocol($path);
150 $this->isFlushed = false;
151 $this->params = $this->getBucketKey($path);
152 $this->mode = rtrim($mode, 'bt');
153
154 if ($errors = $this->validate($path, $this->mode)) {
155 return $this->triggerError($errors);
156 }
157
158 return $this->boolCall(function() {
159 switch ($this->mode) {
160 case 'r': return $this->openReadStream();
161 case 'a': return $this->openAppendStream();
162 default: return $this->openWriteStream();
163 }
164 });
165 }
166
167 public function stream_eof()
168 {
169 return $this->body->eof();
170 }
171
172 public function stream_flush()
173 {
174 // Check if stream body size has been
175 // calculated via a flush or close
176 if($this->body->getSize() === null && $this->mode !== 'r') {
177 return $this->triggerError(
178 "Unable to determine stream size. Did you forget to close or flush the stream?"
179 );
180 }
181
182 $this->isFlushed = true;
183 if ($this->mode == 'r') {
184 return false;
185 }
186
187 if ($this->body->isSeekable()) {
188 $this->body->seek(0);
189 }
190 $params = $this->getOptions(true);
191 $params['Body'] = $this->body;
192
193 // Attempt to guess the ContentType of the upload based on the
194 // file extension of the key
195 if (!isset($params['ContentType']) &&
196 ($type = Psr7\MimeType::fromFilename($params['Key']))
197 ) {
198 $params['ContentType'] = $type;
199 }
200
201 $this->clearCacheKey("{$this->protocol}://{$params['Bucket']}/{$params['Key']}");
202 return $this->boolCall(function () use ($params) {
203 return (bool) $this->getClient()->putObject($params);
204 });
205 }
206
207 public function stream_read($count)
208 {
209 return $this->body->read($count);
210 }
211
212 public function stream_seek($offset, $whence = SEEK_SET)
213 {
214 return !$this->body->isSeekable()
215 ? false
216 : $this->boolCall(function () use ($offset, $whence) {
217 $this->body->seek($offset, $whence);
218 return true;
219 });
220 }
221
222 public function stream_tell()
223 {
224 return $this->boolCall(function() { return $this->body->tell(); });
225 }
226
227 public function stream_write($data)
228 {
229 return $this->body->write($data);
230 }
231
232 public function unlink($path)
233 {
234 $this->initProtocol($path);
235
236 return $this->boolCall(function () use ($path) {
237 $this->clearCacheKey($path);
238 $this->getClient()->deleteObject($this->withPath($path));
239 return true;
240 });
241 }
242
243 public function stream_stat()
244 {
245 $stat = $this->getStatTemplate();
246 $stat[7] = $stat['size'] = $this->getSize();
247 $stat[2] = $stat['mode'] = $this->mode;
248
249 return $stat;
250 }
251
252 /**
253 * Provides information for is_dir, is_file, filesize, etc. Works on
254 * buckets, keys, and prefixes.
255 * @link http://www.php.net/manual/en/streamwrapper.url-stat.php
256 */
257 public function url_stat($path, $flags)
258 {
259 $this->initProtocol($path);
260
261 // Some paths come through as S3:// for some reason.
262 $split = explode('://', $path);
263 $path = strtolower($split[0]) . '://' . $split[1];
264
265 // Check if this path is in the url_stat cache
266 if ($value = $this->getCacheStorage()->get($path)) {
267 return $value;
268 }
269
270 $stat = $this->createStat($path, $flags);
271
272 if (is_array($stat)) {
273 $this->getCacheStorage()->set($path, $stat);
274 }
275
276 return $stat;
277 }
278
279 /**
280 * Parse the protocol out of the given path.
281 *
282 * @param $path
283 */
284 private function initProtocol($path)
285 {
286 $parts = explode('://', $path, 2);
287 $this->protocol = $parts[0] ?: 's3';
288 }
289
290 private function createStat($path, $flags)
291 {
292 $this->initProtocol($path);
293 $parts = $this->withPath($path);
294
295 if (!$parts['Key']) {
296 return $this->statDirectory($parts, $path, $flags);
297 }
298
299 return $this->boolCall(function () use ($parts, $path) {
300 try {
301 $result = $this->getClient()->headObject($parts);
302 if (substr($parts['Key'], -1, 1) == '/' &&
303 $result['ContentLength'] == 0
304 ) {
305 // Return as if it is a bucket to account for console
306 // bucket objects (e.g., zero-byte object "foo/")
307 return $this->formatUrlStat($path);
308 }
309
310 // Attempt to stat and cache regular object
311 return $this->formatUrlStat($result->toArray());
312 } catch (S3Exception $e) {
313 // Maybe this isn't an actual key, but a prefix. Do a prefix
314 // listing of objects to determine.
315 $result = $this->getClient()->listObjects([
316 'Bucket' => $parts['Bucket'],
317 'Prefix' => rtrim($parts['Key'], '/') . '/',
318 'MaxKeys' => 1
319 ]);
320 if (!$result['Contents'] && !$result['CommonPrefixes']) {
321 throw new \Exception("File or directory not found: $path");
322 }
323 return $this->formatUrlStat($path);
324 }
325 }, $flags);
326 }
327
328 private function statDirectory($parts, $path, $flags)
329 {
330 // Stat "directories": buckets, or "s3://"
331 $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
332
333 if (!$parts['Bucket'] ||
334 $this->getClient()->$method($parts['Bucket'])
335 ) {
336 return $this->formatUrlStat($path);
337 }
338
339 return $this->triggerError("File or directory not found: $path", $flags);
340 }
341
342 /**
343 * Support for mkdir().
344 *
345 * @param string $path Directory which should be created.
346 * @param int $mode Permissions. 700-range permissions map to
347 * ACL_PUBLIC. 600-range permissions map to
348 * ACL_AUTH_READ. All other permissions map to
349 * ACL_PRIVATE. Expects octal form.
350 * @param int $options A bitwise mask of values, such as
351 * STREAM_MKDIR_RECURSIVE.
352 *
353 * @return bool
354 * @link http://www.php.net/manual/en/streamwrapper.mkdir.php
355 */
356 public function mkdir($path, $mode, $options)
357 {
358 $this->initProtocol($path);
359 $params = $this->withPath($path);
360 $this->clearCacheKey($path);
361 if (!$params['Bucket']) {
362 return false;
363 }
364
365 if (!isset($params['ACL'])) {
366 $params['ACL'] = $this->determineAcl($mode);
367 }
368
369 return empty($params['Key'])
370 ? $this->createBucket($path, $params)
371 : $this->createSubfolder($path, $params);
372 }
373
374 public function rmdir($path, $options)
375 {
376 $this->initProtocol($path);
377 $this->clearCacheKey($path);
378 $params = $this->withPath($path);
379 $client = $this->getClient();
380
381 if (!$params['Bucket']) {
382 return $this->triggerError('You must specify a bucket');
383 }
384
385 return $this->boolCall(function () use ($params, $path, $client) {
386 if (!$params['Key']) {
387 $client->deleteBucket(['Bucket' => $params['Bucket']]);
388 return true;
389 }
390 return $this->deleteSubfolder($path, $params);
391 });
392 }
393
394 /**
395 * Support for opendir().
396 *
397 * The opendir() method of the Amazon S3 stream wrapper supports a stream
398 * context option of "listFilter". listFilter must be a callable that
399 * accepts an associative array of object data and returns true if the
400 * object should be yielded when iterating the keys in a bucket.
401 *
402 * @param string $path The path to the directory
403 * (e.g. "s3://dir[</prefix>]")
404 * @param string $options Unused option variable
405 *
406 * @return bool true on success
407 * @see http://www.php.net/manual/en/function.opendir.php
408 */
409 public function dir_opendir($path, $options)
410 {
411 $this->initProtocol($path);
412 $this->openedPath = $path;
413 $params = $this->withPath($path);
414 $delimiter = $this->getOption('delimiter');
415 /** @var callable $filterFn */
416 $filterFn = $this->getOption('listFilter');
417 $op = ['Bucket' => $params['Bucket']];
418 $this->openedBucket = $params['Bucket'];
419
420 if ($delimiter === null) {
421 $delimiter = '/';
422 }
423
424 if ($delimiter) {
425 $op['Delimiter'] = $delimiter;
426 }
427
428 if ($params['Key']) {
429 $params['Key'] = rtrim($params['Key'], $delimiter) . $delimiter;
430 $op['Prefix'] = $params['Key'];
431 }
432
433 $this->openedBucketPrefix = $params['Key'];
434
435 // Filter our "/" keys added by the console as directories, and ensure
436 // that if a filter function is provided that it passes the filter.
437 $this->objectIterator = \Aws\flatmap(
438 $this->getClient()->getPaginator('ListObjects', $op),
439 function (Result $result) use ($filterFn) {
440 $contentsAndPrefixes = $result->search('[Contents[], CommonPrefixes[]][]');
441 // Filter out dir place holder keys and use the filter fn.
442 return array_filter(
443 $contentsAndPrefixes,
444 function ($key) use ($filterFn) {
445 return (!$filterFn || call_user_func($filterFn, $key))
446 && (!isset($key['Key']) || substr($key['Key'], -1, 1) !== '/');
447 }
448 );
449 }
450 );
451
452 return true;
453 }
454
455 /**
456 * Close the directory listing handles
457 *
458 * @return bool true on success
459 */
460 public function dir_closedir()
461 {
462 $this->objectIterator = null;
463 gc_collect_cycles();
464
465 return true;
466 }
467
468 /**
469 * This method is called in response to rewinddir()
470 *
471 * @return boolean true on success
472 */
473 public function dir_rewinddir()
474 {
475 return $this->boolCall(function() {
476 $this->objectIterator = null;
477 $this->dir_opendir($this->openedPath, null);
478 return true;
479 });
480 }
481
482 /**
483 * This method is called in response to readdir()
484 *
485 * @return string Should return a string representing the next filename, or
486 * false if there is no next file.
487 * @link http://www.php.net/manual/en/function.readdir.php
488 */
489 public function dir_readdir()
490 {
491 // Skip empty result keys
492 if (!$this->objectIterator->valid()) {
493 return false;
494 }
495
496 // First we need to create a cache key. This key is the full path to
497 // then object in s3: protocol://bucket/key.
498 // Next we need to create a result value. The result value is the
499 // current value of the iterator without the opened bucket prefix to
500 // emulate how readdir() works on directories.
501 // The cache key and result value will depend on if this is a prefix
502 // or a key.
503 $cur = $this->objectIterator->current();
504 if (isset($cur['Prefix'])) {
505 // Include "directories". Be sure to strip a trailing "/"
506 // on prefixes.
507 $result = rtrim($cur['Prefix'], '/');
508 $key = $this->formatKey($result);
509 $stat = $this->formatUrlStat($key);
510 } else {
511 $result = $cur['Key'];
512 $key = $this->formatKey($cur['Key']);
513 $stat = $this->formatUrlStat($cur);
514 }
515
516 // Cache the object data for quick url_stat lookups used with
517 // RecursiveDirectoryIterator.
518 $this->getCacheStorage()->set($key, $stat);
519 $this->objectIterator->next();
520
521 // Remove the prefix from the result to emulate other stream wrappers.
522 return $this->openedBucketPrefix
523 ? substr($result, strlen($this->openedBucketPrefix))
524 : $result;
525 }
526
527 private function formatKey($key)
528 {
529 $protocol = explode('://', $this->openedPath)[0];
530 return "{$protocol}://{$this->openedBucket}/{$key}";
531 }
532
533 /**
534 * Called in response to rename() to rename a file or directory. Currently
535 * only supports renaming objects.
536 *
537 * @param string $path_from the path to the file to rename
538 * @param string $path_to the new path to the file
539 *
540 * @return bool true if file was successfully renamed
541 * @link http://www.php.net/manual/en/function.rename.php
542 */
543 public function rename($path_from, $path_to)
544 {
545 // PHP will not allow rename across wrapper types, so we can safely
546 // assume $path_from and $path_to have the same protocol
547 $this->initProtocol($path_from);
548 $partsFrom = $this->withPath($path_from);
549 $partsTo = $this->withPath($path_to);
550 $this->clearCacheKey($path_from);
551 $this->clearCacheKey($path_to);
552
553 if (!$partsFrom['Key'] || !$partsTo['Key']) {
554 return $this->triggerError('The Amazon S3 stream wrapper only '
555 . 'supports copying objects');
556 }
557
558 return $this->boolCall(function () use ($partsFrom, $partsTo) {
559 $options = $this->getOptions(true);
560 // Copy the object and allow overriding default parameters if
561 // desired, but by default copy metadata
562 $this->getClient()->copy(
563 $partsFrom['Bucket'],
564 $partsFrom['Key'],
565 $partsTo['Bucket'],
566 $partsTo['Key'],
567 isset($options['acl']) ? $options['acl'] : 'private',
568 $options
569 );
570 // Delete the original object
571 $this->getClient()->deleteObject([
572 'Bucket' => $partsFrom['Bucket'],
573 'Key' => $partsFrom['Key']
574 ] + $options);
575 return true;
576 });
577 }
578
579 public function stream_cast($cast_as)
580 {
581 return false;
582 }
583
584 /**
585 * Validates the provided stream arguments for fopen and returns an array
586 * of errors.
587 */
588 private function validate($path, $mode)
589 {
590 $errors = [];
591
592 if (!$this->getOption('Key')) {
593 $errors[] = 'Cannot open a bucket. You must specify a path in the '
594 . 'form of s3://bucket/key';
595 }
596
597 if (!in_array($mode, ['r', 'w', 'a', 'x'])) {
598 $errors[] = "Mode not supported: {$mode}. "
599 . "Use one 'r', 'w', 'a', or 'x'.";
600 }
601
602 if ($mode === 'x') {
603 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
604
605 if ($this->getClient()->$method(
606 $this->getOption('Bucket'),
607 $this->getOption('Key'),
608 $this->getOptions(true)
609 )) {
610 $errors[] = "{$path} already exists on Amazon S3";
611 }
612 }
613
614 return $errors;
615 }
616
617 /**
618 * Get the stream context options available to the current stream
619 *
620 * @param bool $removeContextData Set to true to remove contextual kvp's
621 * like 'client' from the result.
622 *
623 * @return array
624 */
625 private function getOptions($removeContextData = false)
626 {
627 // Context is not set when doing things like stat
628 if ($this->context === null) {
629 $options = [];
630 } else {
631 $options = stream_context_get_options($this->context);
632 $options = isset($options[$this->protocol])
633 ? $options[$this->protocol]
634 : [];
635 }
636
637 $default = stream_context_get_options(stream_context_get_default());
638 $default = isset($default[$this->protocol])
639 ? $default[$this->protocol]
640 : [];
641 $result = $this->params + $options + $default;
642
643 if ($removeContextData) {
644 unset($result['client'], $result['seekable'], $result['cache']);
645 }
646
647 return $result;
648 }
649
650 /**
651 * Get a specific stream context option
652 *
653 * @param string $name Name of the option to retrieve
654 *
655 * @return mixed|null
656 */
657 private function getOption($name)
658 {
659 $options = $this->getOptions();
660
661 return isset($options[$name]) ? $options[$name] : null;
662 }
663
664 /**
665 * Gets the client from the stream context
666 *
667 * @return S3ClientInterface
668 * @throws \RuntimeException if no client has been configured
669 */
670 private function getClient()
671 {
672 if (!$client = $this->getOption('client')) {
673 throw new \RuntimeException('No client in stream context');
674 }
675
676 return $client;
677 }
678
679 private function getBucketKey($path)
680 {
681 // Remove the protocol
682 $parts = explode('://', $path);
683 // Get the bucket, key
684 $parts = explode('/', $parts[1], 2);
685
686 return [
687 'Bucket' => $parts[0],
688 'Key' => isset($parts[1]) ? $parts[1] : null
689 ];
690 }
691
692 /**
693 * Get the bucket and key from the passed path (e.g. s3://bucket/key)
694 *
695 * @param string $path Path passed to the stream wrapper
696 *
697 * @return array Hash of 'Bucket', 'Key', and custom params from the context
698 */
699 private function withPath($path)
700 {
701 $params = $this->getOptions(true);
702
703 return $this->getBucketKey($path) + $params;
704 }
705
706 private function openReadStream()
707 {
708 $client = $this->getClient();
709 $command = $client->getCommand('GetObject', $this->getOptions(true));
710 $command['@http']['stream'] = true;
711 $result = $client->execute($command);
712 $this->size = $result['ContentLength'];
713 $this->body = $result['Body'];
714
715 // Wrap the body in a caching entity body if seeking is allowed
716 if ($this->getOption('seekable') && !$this->body->isSeekable()) {
717 $this->body = new CachingStream($this->body);
718 }
719
720 return true;
721 }
722
723 private function openWriteStream()
724 {
725 $this->body = new Stream(fopen('php://temp', 'r+'));
726 return true;
727 }
728
729 private function openAppendStream()
730 {
731 try {
732 // Get the body of the object and seek to the end of the stream
733 $client = $this->getClient();
734 $this->body = $client->getObject($this->getOptions(true))['Body'];
735 $this->body->seek(0, SEEK_END);
736 return true;
737 } catch (S3Exception $e) {
738 // The object does not exist, so use a simple write stream
739 return $this->openWriteStream();
740 }
741 }
742
743 /**
744 * Trigger one or more errors
745 *
746 * @param string|array $errors Errors to trigger
747 * @param mixed $flags If set to STREAM_URL_STAT_QUIET, then no
748 * error or exception occurs
749 *
750 * @return bool Returns false
751 * @throws \RuntimeException if throw_errors is true
752 */
753 private function triggerError($errors, $flags = null)
754 {
755 // This is triggered with things like file_exists()
756 if ($flags & STREAM_URL_STAT_QUIET) {
757 return $flags & STREAM_URL_STAT_LINK
758 // This is triggered for things like is_link()
759 ? $this->formatUrlStat(false)
760 : false;
761 }
762
763 // This is triggered when doing things like lstat() or stat()
764 trigger_error(implode("\n", (array) $errors), E_USER_WARNING);
765
766 return false;
767 }
768
769 /**
770 * Prepare a url_stat result array
771 *
772 * @param string|array $result Data to add
773 *
774 * @return array Returns the modified url_stat result
775 */
776 private function formatUrlStat($result = null)
777 {
778 $stat = $this->getStatTemplate();
779 switch (gettype($result)) {
780 case 'NULL':
781 case 'string':
782 // Directory with 0777 access - see "man 2 stat".
783 $stat['mode'] = $stat[2] = 0040777;
784 break;
785 case 'array':
786 // Regular file with 0777 access - see "man 2 stat".
787 $stat['mode'] = $stat[2] = 0100777;
788 // Pluck the content-length if available.
789 if (isset($result['ContentLength'])) {
790 $stat['size'] = $stat[7] = $result['ContentLength'];
791 } elseif (isset($result['Size'])) {
792 $stat['size'] = $stat[7] = $result['Size'];
793 }
794 if (isset($result['LastModified'])) {
795 // ListObjects or HeadObject result
796 $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10]
797 = strtotime($result['LastModified']);
798 }
799 }
800
801 return $stat;
802 }
803
804 /**
805 * Creates a bucket for the given parameters.
806 *
807 * @param string $path Stream wrapper path
808 * @param array $params A result of StreamWrapper::withPath()
809 *
810 * @return bool Returns true on success or false on failure
811 */
812 private function createBucket($path, array $params)
813 {
814 $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
815
816 if ($this->getClient()->$method($params['Bucket'])) {
817 return $this->triggerError("Bucket already exists: {$path}");
818 }
819
820 unset($params['ACL']);
821 return $this->boolCall(function () use ($params, $path) {
822 $this->getClient()->createBucket($params);
823 $this->clearCacheKey($path);
824 return true;
825 });
826 }
827
828 /**
829 * Creates a pseudo-folder by creating an empty "/" suffixed key
830 *
831 * @param string $path Stream wrapper path
832 * @param array $params A result of StreamWrapper::withPath()
833 *
834 * @return bool
835 */
836 private function createSubfolder($path, array $params)
837 {
838 // Ensure the path ends in "/" and the body is empty.
839 $params['Key'] = rtrim($params['Key'], '/') . '/';
840 $params['Body'] = '';
841
842 // Fail if this pseudo directory key already exists
843 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
844
845 if ($this->getClient()->$method(
846 $params['Bucket'],
847 $params['Key']
848 )) {
849 return $this->triggerError("Subfolder already exists: {$path}");
850 }
851
852 return $this->boolCall(function () use ($params, $path) {
853 $this->getClient()->putObject($params);
854 $this->clearCacheKey($path);
855 return true;
856 });
857 }
858
859 /**
860 * Deletes a nested subfolder if it is empty.
861 *
862 * @param string $path Path that is being deleted (e.g., 's3://a/b/c')
863 * @param array $params A result of StreamWrapper::withPath()
864 *
865 * @return bool
866 */
867 private function deleteSubfolder($path, $params)
868 {
869 // Use a key that adds a trailing slash if needed.
870 $prefix = rtrim($params['Key'], '/') . '/';
871 $result = $this->getClient()->listObjects([
872 'Bucket' => $params['Bucket'],
873 'Prefix' => $prefix,
874 'MaxKeys' => 1
875 ]);
876
877 // Check if the bucket contains keys other than the placeholder
878 if ($contents = $result['Contents']) {
879 return (count($contents) > 1 || $contents[0]['Key'] != $prefix)
880 ? $this->triggerError('Subfolder is not empty')
881 : $this->unlink(rtrim($path, '/') . '/');
882 }
883
884 return $result['CommonPrefixes']
885 ? $this->triggerError('Subfolder contains nested folders')
886 : true;
887 }
888
889 /**
890 * Determine the most appropriate ACL based on a file mode.
891 *
892 * @param int $mode File mode
893 *
894 * @return string
895 */
896 private function determineAcl($mode)
897 {
898 switch (substr(decoct($mode), 0, 1)) {
899 case '7': return 'public-read';
900 case '6': return 'authenticated-read';
901 default: return 'private';
902 }
903 }
904
905 /**
906 * Gets a URL stat template with default values
907 *
908 * @return array
909 */
910 private function getStatTemplate()
911 {
912 return [
913 0 => 0, 'dev' => 0,
914 1 => 0, 'ino' => 0,
915 2 => 0, 'mode' => 0,
916 3 => 0, 'nlink' => 0,
917 4 => 0, 'uid' => 0,
918 5 => 0, 'gid' => 0,
919 6 => -1, 'rdev' => -1,
920 7 => 0, 'size' => 0,
921 8 => 0, 'atime' => 0,
922 9 => 0, 'mtime' => 0,
923 10 => 0, 'ctime' => 0,
924 11 => -1, 'blksize' => -1,
925 12 => -1, 'blocks' => -1,
926 ];
927 }
928
929 /**
930 * Invokes a callable and triggers an error if an exception occurs while
931 * calling the function.
932 *
933 * @param callable $fn
934 * @param int $flags
935 *
936 * @return bool
937 */
938 private function boolCall(callable $fn, $flags = null)
939 {
940 try {
941 return $fn();
942 } catch (\Exception $e) {
943 return $this->triggerError($e->getMessage(), $flags);
944 }
945 }
946
947 /**
948 * @return LruArrayCache
949 */
950 private function getCacheStorage()
951 {
952 if (!$this->cache) {
953 $this->cache = $this->getOption('cache') ?: new LruArrayCache();
954 }
955
956 return $this->cache;
957 }
958
959 /**
960 * Clears a specific stat cache value from the stat cache and LRU cache.
961 *
962 * @param string $key S3 path (s3://bucket/key).
963 */
964 private function clearCacheKey($key)
965 {
966 clearstatcache(true, $key);
967 $this->getCacheStorage()->remove($key);
968 }
969
970 /**
971 * Returns the size of the opened object body.
972 *
973 * @return int|null
974 */
975 private function getSize()
976 {
977 $size = $this->body->getSize();
978
979 return !empty($size) ? $size : $this->size;
980 }
981 }
982