PluginProbe
Media Cloud Sync / 1.1.1
Media Cloud Sync v1.1.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.1.1, at includes/sdk/s3/Aws/S3/StreamWrapper.php

785 lines 29.6 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);
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 /**
473 * Validates the provided stream arguments for fopen and returns an array
474 * of errors.
475 */
476 private function validate($path, $mode)
477 {
478 $errors = [];
479 if (!$this->getOption('Key')) {
480 $errors[] = 'Cannot open a bucket. You must specify a path in the ' . 'form of s3://bucket/key';
481 }
482 if (!\in_array($mode, ['r', 'w', 'a', 'x'])) {
483 $errors[] = "Mode not supported: {$mode}. " . "Use one 'r', 'w', 'a', or 'x'.";
484 }
485 if ($mode === 'x') {
486 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
487 if ($this->getClient()->{$method}($this->getOption('Bucket'), $this->getOption('Key'), $this->getOptions(\true))) {
488 $errors[] = "{$path} already exists on Amazon S3";
489 }
490 }
491 return $errors;
492 }
493 /**
494 * Get the stream context options available to the current stream
495 *
496 * @param bool $removeContextData Set to true to remove contextual kvp's
497 * like 'client' from the result.
498 *
499 * @return array
500 */
501 private function getOptions($removeContextData = \false)
502 {
503 // Context is not set when doing things like stat
504 if ($this->context === null) {
505 $options = [];
506 } else {
507 $options = \stream_context_get_options($this->context);
508 $options = isset($options[$this->protocol]) ? $options[$this->protocol] : [];
509 }
510 $default = \stream_context_get_options(\stream_context_get_default());
511 $default = isset($default[$this->protocol]) ? $default[$this->protocol] : [];
512 $result = $this->params + $options + $default;
513 if ($removeContextData) {
514 unset($result['client'], $result['seekable'], $result['cache']);
515 }
516 return $result;
517 }
518 /**
519 * Get a specific stream context option
520 *
521 * @param string $name Name of the option to retrieve
522 *
523 * @return mixed|null
524 */
525 private function getOption($name)
526 {
527 $options = $this->getOptions();
528 return isset($options[$name]) ? $options[$name] : null;
529 }
530 /**
531 * Gets the client from the stream context
532 *
533 * @return S3ClientInterface
534 * @throws \RuntimeException if no client has been configured
535 */
536 private function getClient()
537 {
538 if (!($client = $this->getOption('client'))) {
539 throw new \RuntimeException('No client in stream context');
540 }
541 return $client;
542 }
543 private function getBucketKey($path)
544 {
545 // Remove the protocol
546 $parts = \explode('://', $path);
547 // Get the bucket, key
548 $parts = \explode('/', $parts[1], 2);
549 return ['Bucket' => $parts[0], 'Key' => isset($parts[1]) ? $parts[1] : null];
550 }
551 /**
552 * Get the bucket and key from the passed path (e.g. s3://bucket/key)
553 *
554 * @param string $path Path passed to the stream wrapper
555 *
556 * @return array Hash of 'Bucket', 'Key', and custom params from the context
557 */
558 private function withPath($path)
559 {
560 $params = $this->getOptions(\true);
561 return $this->getBucketKey($path) + $params;
562 }
563 private function openReadStream()
564 {
565 $client = $this->getClient();
566 $command = $client->getCommand('GetObject', $this->getOptions(\true));
567 $command['@http']['stream'] = \true;
568 $result = $client->execute($command);
569 $this->size = $result['ContentLength'];
570 $this->body = $result['Body'];
571 // Wrap the body in a caching entity body if seeking is allowed
572 if ($this->getOption('seekable') && !$this->body->isSeekable()) {
573 $this->body = new CachingStream($this->body);
574 }
575 return \true;
576 }
577 private function openWriteStream()
578 {
579 $this->body = new Stream(\fopen('php://temp', 'r+'));
580 return \true;
581 }
582 private function openAppendStream()
583 {
584 try {
585 // Get the body of the object and seek to the end of the stream
586 $client = $this->getClient();
587 $this->body = $client->getObject($this->getOptions(\true))['Body'];
588 $this->body->seek(0, \SEEK_END);
589 return \true;
590 } catch (S3Exception $e) {
591 // The object does not exist, so use a simple write stream
592 return $this->openWriteStream();
593 }
594 }
595 /**
596 * Trigger one or more errors
597 *
598 * @param string|array $errors Errors to trigger
599 * @param mixed $flags If set to STREAM_URL_STAT_QUIET, then no
600 * error or exception occurs
601 *
602 * @return bool Returns false
603 * @throws \RuntimeException if throw_errors is true
604 */
605 private function triggerError($errors, $flags = null)
606 {
607 // This is triggered with things like file_exists()
608 if ($flags & \STREAM_URL_STAT_QUIET) {
609 return $flags & \STREAM_URL_STAT_LINK ? $this->formatUrlStat(\false) : \false;
610 }
611 // This is triggered when doing things like lstat() or stat()
612 \trigger_error(\implode("\n", (array) $errors), \E_USER_WARNING);
613 return \false;
614 }
615 /**
616 * Prepare a url_stat result array
617 *
618 * @param string|array $result Data to add
619 *
620 * @return array Returns the modified url_stat result
621 */
622 private function formatUrlStat($result = null)
623 {
624 $stat = $this->getStatTemplate();
625 switch (\gettype($result)) {
626 case 'NULL':
627 case 'string':
628 // Directory with 0777 access - see "man 2 stat".
629 $stat['mode'] = $stat[2] = 040777;
630 break;
631 case 'array':
632 // Regular file with 0777 access - see "man 2 stat".
633 $stat['mode'] = $stat[2] = 0100777;
634 // Pluck the content-length if available.
635 if (isset($result['ContentLength'])) {
636 $stat['size'] = $stat[7] = $result['ContentLength'];
637 } elseif (isset($result['Size'])) {
638 $stat['size'] = $stat[7] = $result['Size'];
639 }
640 if (isset($result['LastModified'])) {
641 // ListObjects or HeadObject result
642 $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10] = \strtotime($result['LastModified']);
643 }
644 }
645 return $stat;
646 }
647 /**
648 * Creates a bucket for the given parameters.
649 *
650 * @param string $path Stream wrapper path
651 * @param array $params A result of StreamWrapper::withPath()
652 *
653 * @return bool Returns true on success or false on failure
654 */
655 private function createBucket($path, array $params)
656 {
657 $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
658 if ($this->getClient()->{$method}($params['Bucket'])) {
659 return $this->triggerError("Bucket already exists: {$path}");
660 }
661 return $this->boolCall(function () use($params, $path) {
662 $this->getClient()->createBucket($params);
663 $this->clearCacheKey($path);
664 return \true;
665 });
666 }
667 /**
668 * Creates a pseudo-folder by creating an empty "/" suffixed key
669 *
670 * @param string $path Stream wrapper path
671 * @param array $params A result of StreamWrapper::withPath()
672 *
673 * @return bool
674 */
675 private function createSubfolder($path, array $params)
676 {
677 // Ensure the path ends in "/" and the body is empty.
678 $params['Key'] = \rtrim($params['Key'], '/') . '/';
679 $params['Body'] = '';
680 // Fail if this pseudo directory key already exists
681 $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
682 if ($this->getClient()->{$method}($params['Bucket'], $params['Key'])) {
683 return $this->triggerError("Subfolder already exists: {$path}");
684 }
685 return $this->boolCall(function () use($params, $path) {
686 $this->getClient()->putObject($params);
687 $this->clearCacheKey($path);
688 return \true;
689 });
690 }
691 /**
692 * Deletes a nested subfolder if it is empty.
693 *
694 * @param string $path Path that is being deleted (e.g., 's3://a/b/c')
695 * @param array $params A result of StreamWrapper::withPath()
696 *
697 * @return bool
698 */
699 private function deleteSubfolder($path, $params)
700 {
701 // Use a key that adds a trailing slash if needed.
702 $prefix = \rtrim($params['Key'], '/') . '/';
703 $result = $this->getClient()->listObjects(['Bucket' => $params['Bucket'], 'Prefix' => $prefix, 'MaxKeys' => 1]);
704 // Check if the bucket contains keys other than the placeholder
705 if ($contents = $result['Contents']) {
706 return \count($contents) > 1 || $contents[0]['Key'] != $prefix ? $this->triggerError('Subfolder is not empty') : $this->unlink(\rtrim($path, '/') . '/');
707 }
708 return $result['CommonPrefixes'] ? $this->triggerError('Subfolder contains nested folders') : \true;
709 }
710 /**
711 * Determine the most appropriate ACL based on a file mode.
712 *
713 * @param int $mode File mode
714 *
715 * @return string
716 */
717 private function determineAcl($mode)
718 {
719 switch (\substr(\decoct($mode), 0, 1)) {
720 case '7':
721 return 'public-read';
722 case '6':
723 return 'authenticated-read';
724 default:
725 return 'private';
726 }
727 }
728 /**
729 * Gets a URL stat template with default values
730 *
731 * @return array
732 */
733 private function getStatTemplate()
734 {
735 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];
736 }
737 /**
738 * Invokes a callable and triggers an error if an exception occurs while
739 * calling the function.
740 *
741 * @param callable $fn
742 * @param int $flags
743 *
744 * @return bool
745 */
746 private function boolCall(callable $fn, $flags = null)
747 {
748 try {
749 return $fn();
750 } catch (\Exception $e) {
751 return $this->triggerError($e->getMessage(), $flags);
752 }
753 }
754 /**
755 * @return LruArrayCache
756 */
757 private function getCacheStorage()
758 {
759 if (!$this->cache) {
760 $this->cache = $this->getOption('cache') ?: new LruArrayCache();
761 }
762 return $this->cache;
763 }
764 /**
765 * Clears a specific stat cache value from the stat cache and LRU cache.
766 *
767 * @param string $key S3 path (s3://bucket/key).
768 */
769 private function clearCacheKey($key)
770 {
771 \clearstatcache(\true, $key);
772 $this->getCacheStorage()->remove($key);
773 }
774 /**
775 * Returns the size of the opened object body.
776 *
777 * @return int|null
778 */
779 private function getSize()
780 {
781 $size = $this->body->getSize();
782 return !empty($size) ? $size : $this->size;
783 }
784 }
785