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