Completed
Push — master ( 193986...cdf399 )
by Andrey
11:47
created

StreamWrapper::stream_seek()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
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
    /**
98
     * Register the 's3://' stream wrapper
99
     *
100
     * @param S3ClientInterface $client   Client to use with the stream wrapper
101
     * @param string            $protocol Protocol to register as.
102
     * @param CacheInterface    $cache    Default cache for the protocol.
103
     */
104
    public static function register(
105
        S3ClientInterface $client,
106
        $protocol = 's3',
107
        CacheInterface $cache = null
108
    ) {
109
        if (in_array($protocol, stream_get_wrappers())) {
110
            stream_wrapper_unregister($protocol);
111
        }
112
113
        // Set the client passed in as the default stream context client
114
        stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL);
115
        $default = stream_context_get_options(stream_context_get_default());
116
        $default[$protocol]['client'] = $client;
117
118
        if ($cache) {
119
            $default[$protocol]['cache'] = $cache;
120
        } elseif (!isset($default[$protocol]['cache'])) {
121
            // Set a default cache adapter.
122
            $default[$protocol]['cache'] = new LruArrayCache();
123
        }
124
125
        stream_context_set_default($default);
126
    }
127
128
    public function stream_close()
129
    {
130
        $this->body = $this->cache = null;
131
    }
132
133
    public function stream_open($path, $mode, $options, &$opened_path)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $opened_path is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
134
    {
135
        $this->initProtocol($path);
136
        $this->params = $this->getBucketKey($path);
137
        $this->mode = rtrim($mode, 'bt');
138
139
        if ($errors = $this->validate($path, $this->mode)) {
140
            return $this->triggerError($errors);
141
        }
142
143
        return $this->boolCall(function() use ($path) {
144
            switch ($this->mode) {
145
                case 'r': return $this->openReadStream($path);
0 ignored issues
show
Unused Code introduced by
The call to StreamWrapper::openReadStream() has too many arguments starting with $path.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
146
                case 'a': return $this->openAppendStream($path);
0 ignored issues
show
Unused Code introduced by
The call to StreamWrapper::openAppendStream() has too many arguments starting with $path.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
147
                default: return $this->openWriteStream($path);
0 ignored issues
show
Unused Code introduced by
The call to StreamWrapper::openWriteStream() has too many arguments starting with $path.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
148
            }
149
        });
150
    }
151
152
    public function stream_eof()
153
    {
154
        return $this->body->eof();
155
    }
156
157
    public function stream_flush()
158
    {
159
        if ($this->mode == 'r') {
160
            return false;
161
        }
162
163
        if ($this->body->isSeekable()) {
164
            $this->body->seek(0);
165
        }
166
        $params = $this->getOptions(true);
167
        $params['Body'] = $this->body;
168
169
        // Attempt to guess the ContentType of the upload based on the
170
        // file extension of the key
171
        if (!isset($params['ContentType']) &&
172
            ($type = Psr7\mimetype_from_filename($params['Key']))
173
        ) {
174
            $params['ContentType'] = $type;
175
        }
176
177
        $this->clearCacheKey("s3://{$params['Bucket']}/{$params['Key']}");
178
        return $this->boolCall(function () use ($params) {
179
            return (bool) $this->getClient()->putObject($params);
0 ignored issues
show
Documentation Bug introduced by
The method putObject does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
180
        });
181
    }
182
183
    public function stream_read($count)
184
    {
185
        return $this->body->read($count);
186
    }
187
188
    public function stream_seek($offset, $whence = SEEK_SET)
189
    {
190
        return !$this->body->isSeekable()
191
            ? false
192
            : $this->boolCall(function () use ($offset, $whence) {
193
                $this->body->seek($offset, $whence);
194
                return true;
195
            });
196
    }
197
198
    public function stream_tell()
199
    {
200
        return $this->boolCall(function() { return $this->body->tell(); });
201
    }
202
203
    public function stream_write($data)
204
    {
205
        return $this->body->write($data);
206
    }
207
208
    public function unlink($path)
209
    {
210
        $this->initProtocol($path);
211
212
        return $this->boolCall(function () use ($path) {
213
            $this->clearCacheKey($path);
214
            $this->getClient()->deleteObject($this->withPath($path));
0 ignored issues
show
Documentation Bug introduced by
The method deleteObject does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
215
            return true;
216
        });
217
    }
218
219
    public function stream_stat()
220
    {
221
        $stat = $this->getStatTemplate();
222
        $stat[7] = $stat['size'] = $this->getSize();
223
        $stat[2] = $stat['mode'] = $this->mode;
224
225
        return $stat;
226
    }
227
228
    /**
229
     * Provides information for is_dir, is_file, filesize, etc. Works on
230
     * buckets, keys, and prefixes.
231
     * @link http://www.php.net/manual/en/streamwrapper.url-stat.php
232
     */
233
    public function url_stat($path, $flags)
234
    {
235
        $this->initProtocol($path);
236
237
        // Some paths come through as S3:// for some reason.
238
        $split = explode('://', $path);
239
        $path = strtolower($split[0]) . '://' . $split[1];
240
241
        // Check if this path is in the url_stat cache
242
        if ($value = $this->getCacheStorage()->get($path)) {
243
            return $value;
244
        }
245
246
        $stat = $this->createStat($path, $flags);
247
248
        if (is_array($stat)) {
249
            $this->getCacheStorage()->set($path, $stat);
250
        }
251
252
        return $stat;
253
    }
254
255
    /**
256
     * Parse the protocol out of the given path.
257
     *
258
     * @param $path
259
     */
260
    private function initProtocol($path)
261
    {
262
        $parts = explode('://', $path, 2);
263
        $this->protocol = $parts[0] ?: 's3';
264
    }
265
266
    private function createStat($path, $flags)
267
    {
268
        $this->initProtocol($path);
269
        $parts = $this->withPath($path);
270
271
        if (!$parts['Key']) {
272
            return $this->statDirectory($parts, $path, $flags);
273
        }
274
275
        return $this->boolCall(function () use ($parts, $path) {
276
            try {
277
                $result = $this->getClient()->headObject($parts);
0 ignored issues
show
Documentation Bug introduced by
The method headObject does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
278
                if (substr($parts['Key'], -1, 1) == '/' &&
279
                    $result['ContentLength'] == 0
280
                ) {
281
                    // Return as if it is a bucket to account for console
282
                    // bucket objects (e.g., zero-byte object "foo/")
283
                    return $this->formatUrlStat($path);
284
                } else {
285
                    // Attempt to stat and cache regular object
286
                    return $this->formatUrlStat($result->toArray());
287
                }
288
            } catch (S3Exception $e) {
289
                // Maybe this isn't an actual key, but a prefix. Do a prefix
290
                // listing of objects to determine.
291
                $result = $this->getClient()->listObjects([
0 ignored issues
show
Documentation Bug introduced by
The method listObjects does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
292
                    'Bucket'  => $parts['Bucket'],
293
                    'Prefix'  => rtrim($parts['Key'], '/') . '/',
294
                    'MaxKeys' => 1
295
                ]);
296
                if (!$result['Contents'] && !$result['CommonPrefixes']) {
297
                    throw new \Exception("File or directory not found: $path");
298
                }
299
                return $this->formatUrlStat($path);
300
            }
301
        }, $flags);
302
    }
303
304
    private function statDirectory($parts, $path, $flags)
305
    {
306
        // Stat "directories": buckets, or "s3://"
307
        if (!$parts['Bucket'] ||
308
            $this->getClient()->doesBucketExist($parts['Bucket'])
309
        ) {
310
            return $this->formatUrlStat($path);
311
        }
312
313
        return $this->triggerError("File or directory not found: $path", $flags);
314
    }
315
316
    /**
317
     * Support for mkdir().
318
     *
319
     * @param string $path    Directory which should be created.
320
     * @param int    $mode    Permissions. 700-range permissions map to
321
     *                        ACL_PUBLIC. 600-range permissions map to
322
     *                        ACL_AUTH_READ. All other permissions map to
323
     *                        ACL_PRIVATE. Expects octal form.
324
     * @param int    $options A bitwise mask of values, such as
325
     *                        STREAM_MKDIR_RECURSIVE.
326
     *
327
     * @return bool
328
     * @link http://www.php.net/manual/en/streamwrapper.mkdir.php
329
     */
330
    public function mkdir($path, $mode, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
331
    {
332
        $this->initProtocol($path);
333
        $params = $this->withPath($path);
334
        $this->clearCacheKey($path);
335
        if (!$params['Bucket']) {
336
            return false;
337
        }
338
339
        if (!isset($params['ACL'])) {
340
            $params['ACL'] = $this->determineAcl($mode);
341
        }
342
343
        return empty($params['Key'])
344
            ? $this->createBucket($path, $params)
345
            : $this->createSubfolder($path, $params);
346
    }
347
348
    public function rmdir($path, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
349
    {
350
        $this->initProtocol($path);
351
        $this->clearCacheKey($path);
352
        $params = $this->withPath($path);
353
        $client = $this->getClient();
354
355
        if (!$params['Bucket']) {
356
            return $this->triggerError('You must specify a bucket');
357
        }
358
359
        return $this->boolCall(function () use ($params, $path, $client) {
360
            if (!$params['Key']) {
361
                $client->deleteBucket(['Bucket' => $params['Bucket']]);
362
                return true;
363
            }
364
            return $this->deleteSubfolder($path, $params);
365
        });
366
    }
367
368
    /**
369
     * Support for opendir().
370
     *
371
     * The opendir() method of the Amazon S3 stream wrapper supports a stream
372
     * context option of "listFilter". listFilter must be a callable that
373
     * accepts an associative array of object data and returns true if the
374
     * object should be yielded when iterating the keys in a bucket.
375
     *
376
     * @param string $path    The path to the directory
377
     *                        (e.g. "s3://dir[</prefix>]")
378
     * @param string $options Unused option variable
379
     *
380
     * @return bool true on success
381
     * @see http://www.php.net/manual/en/function.opendir.php
382
     */
383
    public function dir_opendir($path, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
384
    {
385
        $this->initProtocol($path);
386
        $this->openedPath = $path;
387
        $params = $this->withPath($path);
388
        $delimiter = $this->getOption('delimiter');
389
        /** @var callable $filterFn */
390
        $filterFn = $this->getOption('listFilter');
391
        $op = ['Bucket' => $params['Bucket']];
392
        $this->openedBucket = $params['Bucket'];
393
394
        if ($delimiter === null) {
395
            $delimiter = '/';
396
        }
397
398
        if ($delimiter) {
399
            $op['Delimiter'] = $delimiter;
400
        }
401
402
        if ($params['Key']) {
403
            $params['Key'] = rtrim($params['Key'], $delimiter) . $delimiter;
404
            $op['Prefix'] = $params['Key'];
405
        }
406
407
        $this->openedBucketPrefix = $params['Key'];
408
409
        // Filter our "/" keys added by the console as directories, and ensure
410
        // that if a filter function is provided that it passes the filter.
411
        $this->objectIterator = \Aws\flatmap(
0 ignored issues
show
Documentation Bug introduced by
It seems like \Aws\flatmap($this->getC...1) !== '/'); }); }) of type object<Generator> is incompatible with the declared type object<Iterator> of property $objectIterator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
412
            $this->getClient()->getPaginator('ListObjects', $op),
413
            function (Result $result) use ($filterFn) {
414
                $contentsAndPrefixes = $result->search('[Contents[], CommonPrefixes[]][]');
415
                // Filter out dir place holder keys and use the filter fn.
416
                return array_filter(
417
                    $contentsAndPrefixes,
418
                    function ($key) use ($filterFn) {
419
                        return (!$filterFn || call_user_func($filterFn, $key))
420
                            && (!isset($key['Key']) || substr($key['Key'], -1, 1) !== '/');
421
                    }
422
                );
423
            }
424
        );
425
426
        return true;
427
    }
428
429
    /**
430
     * Close the directory listing handles
431
     *
432
     * @return bool true on success
433
     */
434
    public function dir_closedir()
435
    {
436
        $this->objectIterator = null;
437
        gc_collect_cycles();
438
439
        return true;
440
    }
441
442
    /**
443
     * This method is called in response to rewinddir()
444
     *
445
     * @return boolean true on success
446
     */
447
    public function dir_rewinddir()
448
    {
449
        $this->boolCall(function() {
450
            $this->objectIterator = null;
451
            $this->dir_opendir($this->openedPath, null);
452
            return true;
453
        });
454
    }
455
456
    /**
457
     * This method is called in response to readdir()
458
     *
459
     * @return string Should return a string representing the next filename, or
460
     *                false if there is no next file.
461
     * @link http://www.php.net/manual/en/function.readdir.php
462
     */
463
    public function dir_readdir()
464
    {
465
        // Skip empty result keys
466
        if (!$this->objectIterator->valid()) {
467
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Aws\S3\StreamWrapper::dir_readdir of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
468
        }
469
470
        // First we need to create a cache key. This key is the full path to
471
        // then object in s3: protocol://bucket/key.
472
        // Next we need to create a result value. The result value is the
473
        // current value of the iterator without the opened bucket prefix to
474
        // emulate how readdir() works on directories.
475
        // The cache key and result value will depend on if this is a prefix
476
        // or a key.
477
        $cur = $this->objectIterator->current();
478
        if (isset($cur['Prefix'])) {
479
            // Include "directories". Be sure to strip a trailing "/"
480
            // on prefixes.
481
            $result = rtrim($cur['Prefix'], '/');
482
            $key = $this->formatKey($result);
483
            $stat = $this->formatUrlStat($key);
484
        } else {
485
            $result = $cur['Key'];
486
            $key = $this->formatKey($cur['Key']);
487
            $stat = $this->formatUrlStat($cur);
488
        }
489
490
        // Cache the object data for quick url_stat lookups used with
491
        // RecursiveDirectoryIterator.
492
        $this->getCacheStorage()->set($key, $stat);
493
        $this->objectIterator->next();
494
495
        // Remove the prefix from the result to emulate other stream wrappers.
496
        return $this->openedBucketPrefix
497
            ? substr($result, strlen($this->openedBucketPrefix))
498
            : $result;
499
    }
500
501
    private function formatKey($key)
502
    {
503
        $protocol = explode('://', $this->openedPath)[0];
504
        return "{$protocol}://{$this->openedBucket}/{$key}";
505
    }
506
507
    /**
508
     * Called in response to rename() to rename a file or directory. Currently
509
     * only supports renaming objects.
510
     *
511
     * @param string $path_from the path to the file to rename
512
     * @param string $path_to   the new path to the file
513
     *
514
     * @return bool true if file was successfully renamed
515
     * @link http://www.php.net/manual/en/function.rename.php
516
     */
517
    public function rename($path_from, $path_to)
518
    {
519
        // PHP will not allow rename across wrapper types, so we can safely
520
        // assume $path_from and $path_to have the same protocol
521
        $this->initProtocol($path_from);
522
        $partsFrom = $this->withPath($path_from);
523
        $partsTo = $this->withPath($path_to);
524
        $this->clearCacheKey($path_from);
525
        $this->clearCacheKey($path_to);
526
527
        if (!$partsFrom['Key'] || !$partsTo['Key']) {
528
            return $this->triggerError('The Amazon S3 stream wrapper only '
529
                . 'supports copying objects');
530
        }
531
532
        return $this->boolCall(function () use ($partsFrom, $partsTo) {
533
            $options = $this->getOptions(true);
534
            // Copy the object and allow overriding default parameters if
535
            // desired, but by default copy metadata
536
            $this->getClient()->copy(
537
                $partsFrom['Bucket'],
538
                $partsFrom['Key'],
539
                $partsTo['Bucket'],
540
                $partsTo['Key'],
541
                isset($options['acl']) ? $options['acl'] : 'private',
542
                $options
543
            );
544
            // Delete the original object
545
            $this->getClient()->deleteObject([
0 ignored issues
show
Documentation Bug introduced by
The method deleteObject does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
546
                'Bucket' => $partsFrom['Bucket'],
547
                'Key'    => $partsFrom['Key']
548
            ] + $options);
549
            return true;
550
        });
551
    }
552
553
    public function stream_cast($cast_as)
0 ignored issues
show
Unused Code introduced by
The parameter $cast_as is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
554
    {
555
        return false;
556
    }
557
558
    /**
559
     * Validates the provided stream arguments for fopen and returns an array
560
     * of errors.
561
     */
562
    private function validate($path, $mode)
563
    {
564
        $errors = [];
565
566
        if (!$this->getOption('Key')) {
567
            $errors[] = 'Cannot open a bucket. You must specify a path in the '
568
                . 'form of s3://bucket/key';
569
        }
570
571
        if (!in_array($mode, ['r', 'w', 'a', 'x'])) {
572
            $errors[] = "Mode not supported: {$mode}. "
573
                . "Use one 'r', 'w', 'a', or 'x'.";
574
        }
575
576
        // When using mode "x" validate if the file exists before attempting
577
        // to read
578
        if ($mode == 'x' &&
579
            $this->getClient()->doesObjectExist(
580
                $this->getOption('Bucket'),
581
                $this->getOption('Key'),
582
                $this->getOptions(true)
583
            )
584
        ) {
585
            $errors[] = "{$path} already exists on Amazon S3";
586
        }
587
588
        return $errors;
589
    }
590
591
    /**
592
     * Get the stream context options available to the current stream
593
     *
594
     * @param bool $removeContextData Set to true to remove contextual kvp's
595
     *                                like 'client' from the result.
596
     *
597
     * @return array
598
     */
599
    private function getOptions($removeContextData = false)
600
    {
601
        // Context is not set when doing things like stat
602
        if ($this->context === null) {
603
            $options = [];
604
        } else {
605
            $options = stream_context_get_options($this->context);
606
            $options = isset($options[$this->protocol])
607
                ? $options[$this->protocol]
608
                : [];
609
        }
610
611
        $default = stream_context_get_options(stream_context_get_default());
612
        $default = isset($default[$this->protocol])
613
            ? $default[$this->protocol]
614
            : [];
615
        $result = $this->params + $options + $default;
616
617
        if ($removeContextData) {
618
            unset($result['client'], $result['seekable'], $result['cache']);
619
        }
620
621
        return $result;
622
    }
623
624
    /**
625
     * Get a specific stream context option
626
     *
627
     * @param string $name Name of the option to retrieve
628
     *
629
     * @return mixed|null
630
     */
631
    private function getOption($name)
632
    {
633
        $options = $this->getOptions();
634
635
        return isset($options[$name]) ? $options[$name] : null;
636
    }
637
638
    /**
639
     * Gets the client from the stream context
640
     *
641
     * @return S3ClientInterface
642
     * @throws \RuntimeException if no client has been configured
643
     */
644
    private function getClient()
645
    {
646
        if (!$client = $this->getOption('client')) {
647
            throw new \RuntimeException('No client in stream context');
648
        }
649
650
        return $client;
651
    }
652
653 View Code Duplication
    private function getBucketKey($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
654
    {
655
        // Remove the protocol
656
        $parts = explode('://', $path);
657
        // Get the bucket, key
658
        $parts = explode('/', $parts[1], 2);
659
660
        return [
661
            'Bucket' => $parts[0],
662
            'Key'    => isset($parts[1]) ? $parts[1] : null
663
        ];
664
    }
665
666
    /**
667
     * Get the bucket and key from the passed path (e.g. s3://bucket/key)
668
     *
669
     * @param string $path Path passed to the stream wrapper
670
     *
671
     * @return array Hash of 'Bucket', 'Key', and custom params from the context
672
     */
673
    private function withPath($path)
674
    {
675
        $params = $this->getOptions(true);
676
677
        return $this->getBucketKey($path) + $params;
678
    }
679
680
    private function openReadStream()
681
    {
682
        $client = $this->getClient();
683
        $command = $client->getCommand('GetObject', $this->getOptions(true));
684
        $command['@http']['stream'] = true;
685
        $result = $client->execute($command);
686
        $this->size = $result['ContentLength'];
687
        $this->body = $result['Body'];
688
689
        // Wrap the body in a caching entity body if seeking is allowed
690
        if ($this->getOption('seekable') && !$this->body->isSeekable()) {
691
            $this->body = new CachingStream($this->body);
692
        }
693
694
        return true;
695
    }
696
697
    private function openWriteStream()
698
    {
699
        $this->body = new Stream(fopen('php://temp', 'r+'));
700
        return true;
701
    }
702
703
    private function openAppendStream()
704
    {
705
        try {
706
            // Get the body of the object and seek to the end of the stream
707
            $client = $this->getClient();
708
            $this->body = $client->getObject($this->getOptions(true))['Body'];
0 ignored issues
show
Bug introduced by
The method getObject() does not exist on Aws\S3\S3ClientInterface. Did you maybe mean getObjectUrl()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
709
            $this->body->seek(0, SEEK_END);
710
            return true;
711
        } catch (S3Exception $e) {
712
            // The object does not exist, so use a simple write stream
713
            return $this->openWriteStream();
714
        }
715
    }
716
717
    /**
718
     * Trigger one or more errors
719
     *
720
     * @param string|array $errors Errors to trigger
721
     * @param mixed        $flags  If set to STREAM_URL_STAT_QUIET, then no
722
     *                             error or exception occurs
723
     *
724
     * @return bool Returns false
725
     * @throws \RuntimeException if throw_errors is true
726
     */
727
    private function triggerError($errors, $flags = null)
728
    {
729
        // This is triggered with things like file_exists()
730
        if ($flags & STREAM_URL_STAT_QUIET) {
731
            return $flags & STREAM_URL_STAT_LINK
732
                // This is triggered for things like is_link()
733
                ? $this->formatUrlStat(false)
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string|array|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
734
                : false;
735
        }
736
737
        // This is triggered when doing things like lstat() or stat()
738
        trigger_error(implode("\n", (array) $errors), E_USER_WARNING);
739
740
        return false;
741
    }
742
743
    /**
744
     * Prepare a url_stat result array
745
     *
746
     * @param string|array $result Data to add
747
     *
748
     * @return array Returns the modified url_stat result
749
     */
750
    private function formatUrlStat($result = null)
751
    {
752
        $stat = $this->getStatTemplate();
753
        switch (gettype($result)) {
754
            case 'NULL':
755
            case 'string':
756
                // Directory with 0777 access - see "man 2 stat".
757
                $stat['mode'] = $stat[2] = 0040777;
758
                break;
759
            case 'array':
760
                // Regular file with 0777 access - see "man 2 stat".
761
                $stat['mode'] = $stat[2] = 0100777;
762
                // Pluck the content-length if available.
763
                if (isset($result['ContentLength'])) {
764
                    $stat['size'] = $stat[7] = $result['ContentLength'];
765
                } elseif (isset($result['Size'])) {
766
                    $stat['size'] = $stat[7] = $result['Size'];
767
                }
768
                if (isset($result['LastModified'])) {
769
                    // ListObjects or HeadObject result
770
                    $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10]
771
                        = strtotime($result['LastModified']);
772
                }
773
        }
774
775
        return $stat;
776
    }
777
778
    /**
779
     * Creates a bucket for the given parameters.
780
     *
781
     * @param string $path   Stream wrapper path
782
     * @param array  $params A result of StreamWrapper::withPath()
783
     *
784
     * @return bool Returns true on success or false on failure
785
     */
786
    private function createBucket($path, array $params)
787
    {
788
        if ($this->getClient()->doesBucketExist($params['Bucket'])) {
789
            return $this->triggerError("Bucket already exists: {$path}");
790
        }
791
792
        return $this->boolCall(function () use ($params, $path) {
793
            $this->getClient()->createBucket($params);
0 ignored issues
show
Documentation Bug introduced by
The method createBucket does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
794
            $this->clearCacheKey($path);
795
            return true;
796
        });
797
    }
798
799
    /**
800
     * Creates a pseudo-folder by creating an empty "/" suffixed key
801
     *
802
     * @param string $path   Stream wrapper path
803
     * @param array  $params A result of StreamWrapper::withPath()
804
     *
805
     * @return bool
806
     */
807
    private function createSubfolder($path, array $params)
808
    {
809
        // Ensure the path ends in "/" and the body is empty.
810
        $params['Key'] = rtrim($params['Key'], '/') . '/';
811
        $params['Body'] = '';
812
813
        // Fail if this pseudo directory key already exists
814
        if ($this->getClient()->doesObjectExist(
815
            $params['Bucket'],
816
            $params['Key'])
817
        ) {
818
            return $this->triggerError("Subfolder already exists: {$path}");
819
        }
820
821
        return $this->boolCall(function () use ($params, $path) {
822
            $this->getClient()->putObject($params);
0 ignored issues
show
Documentation Bug introduced by
The method putObject does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
823
            $this->clearCacheKey($path);
824
            return true;
825
        });
826
    }
827
828
    /**
829
     * Deletes a nested subfolder if it is empty.
830
     *
831
     * @param string $path   Path that is being deleted (e.g., 's3://a/b/c')
832
     * @param array  $params A result of StreamWrapper::withPath()
833
     *
834
     * @return bool
835
     */
836
    private function deleteSubfolder($path, $params)
837
    {
838
        // Use a key that adds a trailing slash if needed.
839
        $prefix = rtrim($params['Key'], '/') . '/';
840
        $result = $this->getClient()->listObjects([
0 ignored issues
show
Documentation Bug introduced by
The method listObjects does not exist on object<Aws\S3\S3ClientInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
841
            'Bucket'  => $params['Bucket'],
842
            'Prefix'  => $prefix,
843
            'MaxKeys' => 1
844
        ]);
845
846
        // Check if the bucket contains keys other than the placeholder
847
        if ($contents = $result['Contents']) {
848
            return (count($contents) > 1 || $contents[0]['Key'] != $prefix)
849
                ? $this->triggerError('Subfolder is not empty')
850
                : $this->unlink(rtrim($path, '/') . '/');
851
        }
852
853
        return $result['CommonPrefixes']
854
            ? $this->triggerError('Subfolder contains nested folders')
855
            : true;
856
    }
857
858
    /**
859
     * Determine the most appropriate ACL based on a file mode.
860
     *
861
     * @param int $mode File mode
862
     *
863
     * @return string
864
     */
865
    private function determineAcl($mode)
866
    {
867
        switch (substr(decoct($mode), 0, 1)) {
868
            case '7': return 'public-read';
869
            case '6': return 'authenticated-read';
870
            default: return 'private';
871
        }
872
    }
873
874
    /**
875
     * Gets a URL stat template with default values
876
     *
877
     * @return array
878
     */
879
    private function getStatTemplate()
880
    {
881
        return [
882
            0  => 0,  'dev'     => 0,
883
            1  => 0,  'ino'     => 0,
884
            2  => 0,  'mode'    => 0,
885
            3  => 0,  'nlink'   => 0,
886
            4  => 0,  'uid'     => 0,
887
            5  => 0,  'gid'     => 0,
888
            6  => -1, 'rdev'    => -1,
889
            7  => 0,  'size'    => 0,
890
            8  => 0,  'atime'   => 0,
891
            9  => 0,  'mtime'   => 0,
892
            10 => 0,  'ctime'   => 0,
893
            11 => -1, 'blksize' => -1,
894
            12 => -1, 'blocks'  => -1,
895
        ];
896
    }
897
898
    /**
899
     * Invokes a callable and triggers an error if an exception occurs while
900
     * calling the function.
901
     *
902
     * @param callable $fn
903
     * @param int      $flags
904
     *
905
     * @return bool
906
     */
907
    private function boolCall(callable $fn, $flags = null)
908
    {
909
        try {
910
            return $fn();
911
        } catch (\Exception $e) {
912
            return $this->triggerError($e->getMessage(), $flags);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->triggerError($e->getMessage(), $flags); of type array<*,integer>|false adds the type array<*,integer> to the return on line 912 which is incompatible with the return type documented by Aws\S3\StreamWrapper::boolCall of type boolean.
Loading history...
913
        }
914
    }
915
916
    /**
917
     * @return LruArrayCache
918
     */
919
    private function getCacheStorage()
920
    {
921
        if (!$this->cache) {
922
            $this->cache = $this->getOption('cache') ?: new LruArrayCache();
923
        }
924
925
        return $this->cache;
926
    }
927
928
    /**
929
     * Clears a specific stat cache value from the stat cache and LRU cache.
930
     *
931
     * @param string $key S3 path (s3://bucket/key).
932
     */
933
    private function clearCacheKey($key)
934
    {
935
        clearstatcache(true, $key);
936
        $this->getCacheStorage()->remove($key);
937
    }
938
939
    /**
940
     * Returns the size of the opened object body.
941
     *
942
     * @return int|null
943
     */
944
    private function getSize()
945
    {
946
        $size = $this->body->getSize();
947
948
        return $size !== null ? $size : $this->size;
949
    }
950
}
951