PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
media-cloud-sync / includes / sdk / s3 / Aws / S3 / StreamWrapper.php

StreamWrapper.php in Media Cloud Sync 1.4.1, at includes/sdk/s3/Aws/S3/StreamWrapper.php

803 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\Aws\S3;
4
5 use Dudlewebs\WPMCS\s3\Aws\CacheInterface;
6 use Dudlewebs\WPMCS\s3\Aws\LruArrayCache;
7 use Dudlewebs\WPMCS\s3\Aws\Result;
8 use Dudlewebs\WPMCS\s3\Aws\S3\Exception\S3Exception;
9 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7;
10 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Stream;
11 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\CachingStream;
12 use Dudlewebs\WPMCS\s3\Psr\Http\Message\StreamInterface;
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 /** @var StreamInterface Underlying stream resource */
67 private $body;
68 /** @var int Size of the body that is opened */
69 private $size;
70 /** @var array Hash of opened stream parameters */
71 private $params = [];
72 /** @var string Mode in which the stream was opened */
73 private $mode;
74 /** @var \Iterator Iterator used with opendir() related calls */
75 private $objectIterator;
76 /** @var string The bucket that was opened when opendir() was called */
77 private $openedBucket;
78 /** @var string The prefix of the bucket that was opened with opendir() */
79 private $openedBucketPrefix;
80 /** @var string Opened bucket path */
81 private $openedPath;
82 /** @var CacheInterface Cache for object and dir lookups */
83 private $cache;
84 /** @var string The opened protocol (e.g., "s3") */
85 private $protocol = 's3';
86 /** @var bool Keeps track of whether stream has been flushed since opening */
87 private $isFlushed = \false;
88 /** @var bool Whether or not to use V2 bucket and object existence methods */
89 private static $useV2Existence = \false;
90 /**
91 * Register the 's3://' stream wrapper
92 *
93 * @param S3ClientInterface $client Client to use with the stream wrapper
94 * @param string $protocol Protocol to register as.
95 * @param CacheInterface $cache Default cache for the protocol.
96 */
97 public static function register(S3ClientInterface $client, $protocol = 's3', ?CacheInterface $cache = null, $v2Existence = \false)
98 {
99 self::$useV2Existence = $v2Existence;
100 if (\in_array($protocol, \stream_get_wrappers())) {
101 \stream_wrapper_unregister($protocol);
102 }
103 // Set the client passed in as the default stream context client
104 \stream_wrapper_register($protocol, \get_called_class(), \STREAM_IS_URL);
105 $default = \stream_context_get_options(\stream_context_get_default());
106 $default[$protocol]['client'] = $client;
107 if ($cache) {
108 $default[$protocol]['cache'] = $cache;
109 } elseif (!isset($default[$protocol]['cache'])) {
110 // Set a default cache adapter.
111 $default[$protocol]['cache'] = new LruArrayCache();
112 }
113 \stream_context_set_default($default);
114 }
115 public function stream_close()
116 {
117 if (!$this->isFlushed && empty($this->body->getSize()) && $this->mode !== 'r') {
118 $this->stream_flush();
119 }
120 $this->body = $this->cache = null;
121 }
122 public function stream_open($path, $mode, $options, &$opened_path)
123 {
124 $this->initProtocol($path);
125 $this->isFlushed = \false;
126 $this->params = $this->getBucketKey($path);
127 $this->mode = \rtrim($mode, 'bt');
128 if ($errors = $this->validate($path, $this->mode)) {
129 return $this->triggerError($errors);
130 }
131 return $this->boolCall(function () {
132 switch ($this->mode) {
133 case 'r':
134 return $this->openReadStream();
135 case 'a':
136 return $this->openAppendStream();
137 default:
138 return $this->openWriteStream();
139 }
140 });
141 }
142 public function stream_eof()
143 {
144 return $this->body->eof();
145 }
146 public function stream_flush()
147 {
148 // Check if stream body size has been
149 // calculated via a flush or close
150 if ($this->body->getSize() === null && $this->mode !== 'r') {
151 return $this->triggerError("Unable to determine stream size. Did you forget to close or flush the stream?");
152 }
153 $this->isFlushed = \true;
154 if ($this->mode == 'r') {
155 return \false;
156 }
157 if ($this->body->isSeekable()) {
158 $this->body->seek(0);
159 }
160 $params = $this->getOptions(\true);
161 $params['Body'] = $this->body;
162 // Attempt to guess the ContentType of the upload based on the
163 // file extension of the key
164 if (!isset($params['ContentType']) && ($type = Psr7\MimeType::fromFilename($params['Key']))) {
165 $params['ContentType'] = $type;
166 }
167 $this->clearCacheKey("{$this->protocol}://{$params['Bucket']}/{$params['Key']}");
168 return $this->boolCall(function () use($params) {
169 return (bool) $this->getClient()->putObject($params);
170 });
171 }
172 public function stream_read($count)
173 {
174 return $this->body->read($count);
175 }
176 public function stream_seek($offset, $whence = \SEEK_SET)
177 {
178 return !$this->body->isSeekable() ? \false : $this->boolCall(function () use($offset, $whence) {
179 $this->body->seek($offset, $whence);
180 return \true;
181 });
182 }
183 public function stream_tell()
184 {
185 return $this->boolCall(function () {
186 return $this->body->tell();
187 });
188 }
189 public function stream_write($data)
190 {
191 return $this->body->write($data);
192 }
193 public function unlink($path)
194 {
195 $this->initProtocol($path);
196 return $this->boolCall(function () use($path) {
197 $this->clearCacheKey($path);
198 $this->getClient()->deleteObject($this->withPath($path));
199 return \true;
200 });
201 }
202 public function stream_stat()
203 {
204 $stat = $this->getStatTemplate();
205 $stat[7] = $stat['size'] = $this->getSize();
206 $stat[2] = $stat['mode'] = $this->mode;
207 return $stat;
208 }
209 /**
210 * Provides information for is_dir, is_file, filesize, etc. Works on
211 * buckets, keys, and prefixes.
212 * @link http://www.php.net/manual/en/streamwrapper.url-stat.php
213 */
214 public function url_stat($path, $flags)
215 {
216 $this->initProtocol($path);
217 // Some paths come through as S3:// for some reason.
218 $split = \explode('://', $path, 2);
219 $path = \strtolower($split[0]) . '://' . $split[1];
220 // Check if this path is in the url_stat cache
221 if ($value = $this->getCacheStorage()->get($path)) {
222 return $value;
223 }
224 $stat = $this->createStat($path, $flags);
225 if (\is_array($stat)) {
226 $this->getCacheStorage()->set($path, $stat);
227 }
228 return $stat;
229 }
230 /**
231 * Parse the protocol out of the given path.
232 *
233 * @param $path
234 */
235 private function initProtocol($path)
236 {
237 $parts = \explode('://', $path, 2);
238 $this->protocol = $parts[0] ?: 's3';
239 }
240 private function createStat($path, $flags)
241 {
242 $this->initProtocol($path);
243 $parts = $this->withPath($path);
244 if (!$parts['Key']) {
245 return $this->statDirectory($parts, $path, $flags);
246 }
247 return $this->boolCall(function () use($parts, $path) {
248 try {
249 $result = $this->getClient()->headObject($parts);
250 if (\substr($parts['Key'], -1, 1) == '/' && $result['ContentLength'] == 0) {
251 // Return as if it is a bucket to account for console
252 // bucket objects (e.g., zero-byte object "foo/")
253 return $this->formatUrlStat($path);
254 }
255 // Attempt to stat and cache regular object
256 return $this->formatUrlStat($result->toArray());
257 } catch (S3Exception $e) {
258 // Maybe this isn't an actual key, but a prefix. Do a prefix
259 // listing of objects to determine.
260 $result = $this->getClient()->listObjects(['Bucket' => $parts['Bucket'], 'Prefix' => \rtrim($parts['Key'], '/') . '/', 'MaxKeys' => 1]);
261 if (!$result['Contents'] && !$result['CommonPrefixes']) {
262 throw new \Exception("File or directory not found: {$path}");
263 }
264 return $this->formatUrlStat($path);
265 }
266 }, $flags);
267 }
268 private function statDirectory($parts, $path, $flags)
269 {
270 // Stat "directories": buckets, or "s3://"
271 $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
272 if (!$parts['Bucket'] || $this->getClient()->{$method}($parts['Bucket'])) {
273 return $this->formatUrlStat($path);
274 }
275 return $this->triggerError("File or directory not found: {$path}", $flags);
276 }
277 /**
278 * Support for mkdir().
279 *
280 * @param string $path Directory which should be created.
281 * @param int $mode Permissions. 700-range permissions map to
282 * ACL_PUBLIC. 600-range permissions map to
283 * ACL_AUTH_READ. All other permissions map to
284 * ACL_PRIVATE. Expects octal form.
285 * @param int $options A bitwise mask of values, such as
286 * STREAM_MKDIR_RECURSIVE.
287 *
288 * @return bool
289 * @link http://www.php.net/manual/en/streamwrapper.mkdir.php
290 */
291 public function mkdir($path, $mode, $options)
292 {
293 $this->initProtocol($path);
294 $params = $this->withPath($path);
295 $this->clearCacheKey($path);
296 if (!$params['Bucket']) {
297 return \false;
298 }
299 if (!isset($params['ACL'])) {
300 $params['ACL'] = $this->determineAcl($mode);
301 }
302 return empty($params['Key']) ? $this->createBucket($path, $params) : $this->createSubfolder($path, $params);
303 }
304 public function rmdir($path, $options)
305 {
306 $this->initProtocol($path);
307 $this->clearCacheKey($path);
308 $params = $this->withPath($path);
309 $client = $this->getClient();
310 if (!$params['Bucket']) {
311 return $this->triggerError('You must specify a bucket');
312 }
313 return $this->boolCall(function () use($params, $path, $client) {
314 if (!$params['Key']) {
315 $client->deleteBucket(['Bucket' => $params['Bucket']]);
316 return \true;
317 }
318 return $this->deleteSubfolder($path, $params);
319 });
320 }
321 /**
322 * Support for opendir().
323 *
324 * The opendir() method of the Amazon S3 stream wrapper supports a stream
325 * context option of "listFilter". listFilter must be a callable that
326 * accepts an associative array of object data and returns true if the
327 * object should be yielded when iterating the keys in a bucket.
328 *
329 * @param string $path The path to the directory
330 * (e.g. "s3://dir[</prefix>]")
331 * @param string $options Unused option variable
332 *
333 * @return bool true on success
334 * @see http://www.php.net/manual/en/function.opendir.php
335 */
336 public function dir_opendir($path, $options)
337 {
338 $this->initProtocol($path);
339 $this->openedPath = $path;
340 $params = $this->withPath($path);
341 $delimiter = $this->getOption('delimiter');
342 /** @var callable $filterFn */
343 $filterFn = $this->getOption('listFilter');
344 $op = ['Bucket' => $params['Bucket']];
345 $this->openedBucket = $params['Bucket'];
346 if ($delimiter === null) {
347 $delimiter = '/';
348 }
349 if ($delimiter) {
350 $op['Delimiter'] = $delimiter;
351 }
352 if ($params['Key']) {
353 $params['Key'] = \rtrim($params['Key'], $delimiter) . $delimiter;
354 $op['Prefix'] = $params['Key'];
355 }
356 $this->openedBucketPrefix = $params['Key'];
357 // Filter our "/" keys added by the console as directories, and ensure
358 // that if a filter function is provided that it passes the filter.
359 $this->objectIterator = \Dudlewebs\WPMCS\s3\Aws\flatmap($this->getClient()->getPaginator('ListObjects', $op), function (Result $result) use($filterFn) {
360 $contentsAndPrefixes = $result->search('[Contents[], CommonPrefixes[]][]');
361 // Filter out dir place holder keys and use the filter fn.
362 return \array_filter($contentsAndPrefixes, function ($key) use($filterFn) {
363 return (!$filterFn || \call_user_func($filterFn, $key)) && (!isset($key['Key']) || \substr($key['Key'], -1, 1) !== '/');
364 });
365 });
366 return \true;
367 }
368 /**
369 * Close the directory listing handles
370 *
371 * @return bool true on success
372 */
373 public function dir_closedir()
374 {
375 $this->objectIterator = null;
376 \gc_collect_cycles();
377 return \true;
378 }
379 /**
380 * This method is called in response to rewinddir()
381 *
382 * @return boolean true on success
383 */
384 public function dir_rewinddir()
385 {
386 return $this->boolCall(function () {
387 $this->objectIterator = null;
388 $this->dir_opendir($this->openedPath, null);
389 return \true;
390 });
391 }
392 /**
393 * This method is called in response to readdir()
394 *
395 * @return string Should return a string representing the next filename, or
396 * false if there is no next file.
397 * @link http://www.php.net/manual/en/function.readdir.php
398 */
399 public function dir_readdir()
400 {
401 // Skip empty result keys
402 if (!$this->objectIterator->valid()) {
403 return \false;
404 }
405 // First we need to create a cache key. This key is the full path to
406 // then object in s3: protocol://bucket/key.
407 // Next we need to create a result value. The result value is the
408 // current value of the iterator without the opened bucket prefix to
409 // emulate how readdir() works on directories.
410 // The cache key and result value will depend on if this is a prefix
411 // or a key.
412 $cur = $this->objectIterator->current();
413 if (isset($cur['Prefix'])) {
414 // Include "directories". Be sure to strip a trailing "/"
415 // on prefixes.
416 $result = \rtrim($cur['Prefix'], '/');
417 $key = $this->formatKey($result);
418 $stat = $this->formatUrlStat($key);
419 } else {
420 $result = $cur['Key'];
421 $key = $this->formatKey($cur['Key']);
422 $stat = $this->formatUrlStat($cur);
423 }
424 // Cache the object data for quick url_stat lookups used with
425 // RecursiveDirectoryIterator.
426 $this->getCacheStorage()->set($key, $stat);
427 $this->objectIterator->next();
428 // Remove the prefix from the result to emulate other stream wrappers.
429 return $this->openedBucketPrefix ? \substr($result, \strlen($this->openedBucketPrefix)) : $result;
430 }
431 private function formatKey($key)
432 {
433 $protocol = \explode('://', $this->openedPath)[0];
434 return "{$protocol}://{$this->openedBucket}/{$key}";
435 }
436 /**
437 * Called in response to rename() to rename a file or directory. Currently
438 * only supports renaming objects.
439 *
440 * @param string $path_from the path to the file to rename
441 * @param string $path_to the new path to the file
442 *
443 * @return bool true if file was successfully renamed
444 * @link http://www.php.net/manual/en/function.rename.php
445 */
446 public function rename($path_from, $path_to)
447 {
448 // PHP will not allow rename across wrapper types, so we can safely
449 // assume $path_from and $path_to have the same protocol
450 $this->initProtocol($path_from);
451 $partsFrom = $this->withPath($path_from);
452 $partsTo = $this->withPath($path_to);
453 $this->clearCacheKey($path_from);
454 $this->clearCacheKey($path_to);
455 if (!$partsFrom['Key'] || !$partsTo['Key']) {
456 return $this->triggerError('The Amazon S3 stream wrapper only ' . 'supports copying objects');
457 }
458 return $this->boolCall(function () use($partsFrom, $partsTo) {
459 $options = $this->getOptions(\true);
460 // Copy the object and allow overriding default parameters if
461 // desired, but by default copy metadata
462 $this->getClient()->copy($partsFrom['Bucket'], $partsFrom['Key'], $partsTo['Bucket'], $partsTo['Key'], isset($options['acl']) ? $options['acl'] : 'private', $options);
463 // Delete the original object
464 $this->getClient()->deleteObject(['Bucket' => $partsFrom['Bucket'], 'Key' => $partsFrom['Key']] + $options);
465 return \true;
466 });
467 }
468 public function stream_cast($cast_as)
469 {
470 return \false;
471 }
472 public function stream_set_option($option, $arg1, $arg2)
473 {
474 return \false;
475 }
476 public function stream_metadata($path, $option, $value)
477 {
478 return \false;
479 }
480 public function stream_lock($operation)
481 {
482 \trigger_error('stream_lock() is not supported by the Amazon S3 stream wrapper', \E_USER_WARNING);
483 return \false;
484 }
485 public function stream_truncate($new_size)
486 {
487 return \false;
488 }
489 /**
490 * Validates the provided stream arguments for fopen and returns an array
491 * of errors.
492 */
493 private function validate($path, $mode)
494 {
495 $errors = [];
496 if (!$this->getOption('Key')) {
497 $errors[] = 'Cannot open a bucket. You must specify a path in the ' . 'form of s3://bucket/key';
498 }
499 if (!\in_array($mode, ['r', 'w', 'a', 'x'])) {
500 $errors[] = "Mode not supported: {$mode}. " . "Use one 'r', 'w', 'a', or 'x'.";
501 }
502 if ($mode === 'x') {
503 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
504 if ($this->getClient()->{$method}($this->getOption('Bucket'), $this->getOption('Key'), $this->getOptions(\true))) {
505 $errors[] = "{$path} already exists on Amazon S3";
506 }
507 }
508 return $errors;
509 }
510 /**
511 * Get the stream context options available to the current stream
512 *
513 * @param bool $removeContextData Set to true to remove contextual kvp's
514 * like 'client' from the result.
515 *
516 * @return array
517 */
518 private function getOptions($removeContextData = \false)
519 {
520 // Context is not set when doing things like stat
521 if ($this->context === null) {
522 $options = [];
523 } else {
524 $options = \stream_context_get_options($this->context);
525 $options = isset($options[$this->protocol]) ? $options[$this->protocol] : [];
526 }
527 $default = \stream_context_get_options(\stream_context_get_default());
528 $default = isset($default[$this->protocol]) ? $default[$this->protocol] : [];
529 $result = $this->params + $options + $default;
530 if ($removeContextData) {
531 unset($result['client'], $result['seekable'], $result['cache']);
532 }
533 return $result;
534 }
535 /**
536 * Get a specific stream context option
537 *
538 * @param string $name Name of the option to retrieve
539 *
540 * @return mixed|null
541 */
542 private function getOption($name)
543 {
544 $options = $this->getOptions();
545 return isset($options[$name]) ? $options[$name] : null;
546 }
547 /**
548 * Gets the client from the stream context
549 *
550 * @return S3ClientInterface
551 * @throws \RuntimeException if no client has been configured
552 */
553 private function getClient()
554 {
555 if (!($client = $this->getOption('client'))) {
556 throw new \RuntimeException('No client in stream context');
557 }
558 return $client;
559 }
560 private function getBucketKey($path)
561 {
562 // Remove the protocol
563 $parts = \explode('://', $path, 2);
564 // Get the bucket, key
565 $parts = \explode('/', $parts[1], 2);
566 return ['Bucket' => $parts[0], 'Key' => isset($parts[1]) ? $parts[1] : null];
567 }
568 /**
569 * Get the bucket and key from the passed path (e.g. s3://bucket/key)
570 *
571 * @param string $path Path passed to the stream wrapper
572 *
573 * @return array Hash of 'Bucket', 'Key', and custom params from the context
574 */
575 private function withPath($path)
576 {
577 $params = $this->getOptions(\true);
578 return $this->getBucketKey($path) + $params;
579 }
580 private function openReadStream()
581 {
582 $client = $this->getClient();
583 $command = $client->getCommand('GetObject', $this->getOptions(\true));
584 $command['@http']['stream'] = \true;
585 $result = $client->execute($command);
586 $this->size = $result['ContentLength'];
587 $this->body = $result['Body'];
588 // Wrap the body in a caching entity body if seeking is allowed
589 if ($this->getOption('seekable') && !$this->body->isSeekable()) {
590 $this->body = new CachingStream($this->body);
591 }
592 return \true;
593 }
594 private function openWriteStream()
595 {
596 $this->body = new Stream(\fopen('php://temp', 'r+'));
597 return \true;
598 }
599 private function openAppendStream()
600 {
601 try {
602 // Get the body of the object and seek to the end of the stream
603 $client = $this->getClient();
604 $this->body = $client->getObject($this->getOptions(\true))['Body'];
605 $this->body->seek(0, \SEEK_END);
606 return \true;
607 } catch (S3Exception $e) {
608 // The object does not exist, so use a simple write stream
609 return $this->openWriteStream();
610 }
611 }
612 /**
613 * Trigger one or more errors
614 *
615 * @param string|array $errors Errors to trigger
616 * @param mixed $flags If set to STREAM_URL_STAT_QUIET, then no
617 * error or exception occurs
618 *
619 * @return bool Returns false
620 * @throws \RuntimeException if throw_errors is true
621 */
622 private function triggerError($errors, $flags = null)
623 {
624 // This is triggered with things like file_exists()
625 if ($flags & \STREAM_URL_STAT_QUIET) {
626 return $flags & \STREAM_URL_STAT_LINK ? $this->formatUrlStat(\false) : \false;
627 }
628 // This is triggered when doing things like lstat() or stat()
629 \trigger_error(\implode("\n", (array) $errors), \E_USER_WARNING);
630 return \false;
631 }
632 /**
633 * Prepare a url_stat result array
634 *
635 * @param string|array $result Data to add
636 *
637 * @return array Returns the modified url_stat result
638 */
639 private function formatUrlStat($result = null)
640 {
641 $stat = $this->getStatTemplate();
642 switch (\gettype($result)) {
643 case 'NULL':
644 case 'string':
645 // Directory with 0777 access - see "man 2 stat".
646 $stat['mode'] = $stat[2] = 040777;
647 break;
648 case 'array':
649 // Regular file with 0777 access - see "man 2 stat".
650 $stat['mode'] = $stat[2] = 0100777;
651 // Pluck the content-length if available.
652 if (isset($result['ContentLength'])) {
653 $stat['size'] = $stat[7] = $result['ContentLength'];
654 } elseif (isset($result['Size'])) {
655 $stat['size'] = $stat[7] = $result['Size'];
656 }
657 if (isset($result['LastModified'])) {
658 // ListObjects or HeadObject result
659 $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10] = \strtotime($result['LastModified']);
660 }
661 }
662 return $stat;
663 }
664 /**
665 * Creates a bucket for the given parameters.
666 *
667 * @param string $path Stream wrapper path
668 * @param array $params A result of StreamWrapper::withPath()
669 *
670 * @return bool Returns true on success or false on failure
671 */
672 private function createBucket($path, array $params)
673 {
674 $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
675 if ($this->getClient()->{$method}($params['Bucket'])) {
676 return $this->triggerError("Bucket already exists: {$path}");
677 }
678 unset($params['ACL']);
679 return $this->boolCall(function () use($params, $path) {
680 $this->getClient()->createBucket($params);
681 $this->clearCacheKey($path);
682 return \true;
683 });
684 }
685 /**
686 * Creates a pseudo-folder by creating an empty "/" suffixed key
687 *
688 * @param string $path Stream wrapper path
689 * @param array $params A result of StreamWrapper::withPath()
690 *
691 * @return bool
692 */
693 private function createSubfolder($path, array $params)
694 {
695 // Ensure the path ends in "/" and the body is empty.
696 $params['Key'] = \rtrim($params['Key'], '/') . '/';
697 $params['Body'] = '';
698 // Fail if this pseudo directory key already exists
699 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
700 if ($this->getClient()->{$method}($params['Bucket'], $params['Key'])) {
701 return $this->triggerError("Subfolder already exists: {$path}");
702 }
703 return $this->boolCall(function () use($params, $path) {
704 $this->getClient()->putObject($params);
705 $this->clearCacheKey($path);
706 return \true;
707 });
708 }
709 /**
710 * Deletes a nested subfolder if it is empty.
711 *
712 * @param string $path Path that is being deleted (e.g., 's3://a/b/c')
713 * @param array $params A result of StreamWrapper::withPath()
714 *
715 * @return bool
716 */
717 private function deleteSubfolder($path, $params)
718 {
719 // Use a key that adds a trailing slash if needed.
720 $prefix = \rtrim($params['Key'], '/') . '/';
721 $result = $this->getClient()->listObjects(['Bucket' => $params['Bucket'], 'Prefix' => $prefix, 'MaxKeys' => 1]);
722 // Check if the bucket contains keys other than the placeholder
723 if ($contents = $result['Contents']) {
724 return \count($contents) > 1 || $contents[0]['Key'] != $prefix ? $this->triggerError('Subfolder is not empty') : $this->unlink(\rtrim($path, '/') . '/');
725 }
726 return $result['CommonPrefixes'] ? $this->triggerError('Subfolder contains nested folders') : \true;
727 }
728 /**
729 * Determine the most appropriate ACL based on a file mode.
730 *
731 * @param int $mode File mode
732 *
733 * @return string
734 */
735 private function determineAcl($mode)
736 {
737 switch (\substr(\decoct($mode), 0, 1)) {
738 case '7':
739 return 'public-read';
740 case '6':
741 return 'authenticated-read';
742 default:
743 return 'private';
744 }
745 }
746 /**
747 * Gets a URL stat template with default values
748 *
749 * @return array
750 */
751 private function getStatTemplate()
752 {
753 return [0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => 0, 'mode' => 0, 3 => 0, 'nlink' => 0, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => -1, 'rdev' => -1, 7 => 0, 'size' => 0, 8 => 0, 'atime' => 0, 9 => 0, 'mtime' => 0, 10 => 0, 'ctime' => 0, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1];
754 }
755 /**
756 * Invokes a callable and triggers an error if an exception occurs while
757 * calling the function.
758 *
759 * @param callable $fn
760 * @param int $flags
761 *
762 * @return bool
763 */
764 private function boolCall(callable $fn, $flags = null)
765 {
766 try {
767 return $fn();
768 } catch (\Exception $e) {
769 return $this->triggerError($e->getMessage(), $flags);
770 }
771 }
772 /**
773 * @return LruArrayCache
774 */
775 private function getCacheStorage()
776 {
777 if (!$this->cache) {
778 $this->cache = $this->getOption('cache') ?: new LruArrayCache();
779 }
780 return $this->cache;
781 }
782 /**
783 * Clears a specific stat cache value from the stat cache and LRU cache.
784 *
785 * @param string $key S3 path (s3://bucket/key).
786 */
787 private function clearCacheKey($key)
788 {
789 \clearstatcache(\true, $key);
790 $this->getCacheStorage()->remove($key);
791 }
792 /**
793 * Returns the size of the opened object body.
794 *
795 * @return int|null
796 */
797 private function getSize()
798 {
799 $size = $this->body->getSize();
800 return !empty($size) ? $size : $this->size;
801 }
802 }
803