123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981 |
- <?php
- namespace Aws\S3;
- use Aws\CacheInterface;
- use Aws\LruArrayCache;
- use Aws\Result;
- use Aws\S3\Exception\S3Exception;
- use GuzzleHttp\Psr7;
- use GuzzleHttp\Psr7\Stream;
- use GuzzleHttp\Psr7\CachingStream;
- use Psr\Http\Message\StreamInterface;
- class StreamWrapper
- {
-
- public $context;
-
- private $body;
-
- private $size;
-
- private $params = [];
-
- private $mode;
-
- private $objectIterator;
-
- private $openedBucket;
-
- private $openedBucketPrefix;
-
- private $openedPath;
-
- private $cache;
-
- private $protocol = 's3';
-
- private $isFlushed = false;
-
- private static $useV2Existence = false;
-
- public static function register(
- S3ClientInterface $client,
- $protocol = 's3',
- CacheInterface $cache = null,
- $v2Existence = false
- ) {
- self::$useV2Existence = $v2Existence;
- if (in_array($protocol, stream_get_wrappers())) {
- stream_wrapper_unregister($protocol);
- }
-
- stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL);
- $default = stream_context_get_options(stream_context_get_default());
- $default[$protocol]['client'] = $client;
- if ($cache) {
- $default[$protocol]['cache'] = $cache;
- } elseif (!isset($default[$protocol]['cache'])) {
-
- $default[$protocol]['cache'] = new LruArrayCache();
- }
- stream_context_set_default($default);
- }
- public function stream_close()
- {
- if (!$this->isFlushed
- && empty($this->body->getSize())
- && $this->mode !== 'r'
- ) {
- $this->stream_flush();
- }
- $this->body = $this->cache = null;
- }
- public function stream_open($path, $mode, $options, &$opened_path)
- {
- $this->initProtocol($path);
- $this->isFlushed = false;
- $this->params = $this->getBucketKey($path);
- $this->mode = rtrim($mode, 'bt');
- if ($errors = $this->validate($path, $this->mode)) {
- return $this->triggerError($errors);
- }
- return $this->boolCall(function() {
- switch ($this->mode) {
- case 'r': return $this->openReadStream();
- case 'a': return $this->openAppendStream();
- default: return $this->openWriteStream();
- }
- });
- }
- public function stream_eof()
- {
- return $this->body->eof();
- }
- public function stream_flush()
- {
-
-
- if($this->body->getSize() === null && $this->mode !== 'r') {
- return $this->triggerError(
- "Unable to determine stream size. Did you forget to close or flush the stream?"
- );
- }
- $this->isFlushed = true;
- if ($this->mode == 'r') {
- return false;
- }
- if ($this->body->isSeekable()) {
- $this->body->seek(0);
- }
- $params = $this->getOptions(true);
- $params['Body'] = $this->body;
-
-
- if (!isset($params['ContentType']) &&
- ($type = Psr7\MimeType::fromFilename($params['Key']))
- ) {
- $params['ContentType'] = $type;
- }
- $this->clearCacheKey("{$this->protocol}://{$params['Bucket']}/{$params['Key']}");
- return $this->boolCall(function () use ($params) {
- return (bool) $this->getClient()->putObject($params);
- });
- }
- public function stream_read($count)
- {
- return $this->body->read($count);
- }
- public function stream_seek($offset, $whence = SEEK_SET)
- {
- return !$this->body->isSeekable()
- ? false
- : $this->boolCall(function () use ($offset, $whence) {
- $this->body->seek($offset, $whence);
- return true;
- });
- }
- public function stream_tell()
- {
- return $this->boolCall(function() { return $this->body->tell(); });
- }
- public function stream_write($data)
- {
- return $this->body->write($data);
- }
- public function unlink($path)
- {
- $this->initProtocol($path);
- return $this->boolCall(function () use ($path) {
- $this->clearCacheKey($path);
- $this->getClient()->deleteObject($this->withPath($path));
- return true;
- });
- }
- public function stream_stat()
- {
- $stat = $this->getStatTemplate();
- $stat[7] = $stat['size'] = $this->getSize();
- $stat[2] = $stat['mode'] = $this->mode;
- return $stat;
- }
-
- public function url_stat($path, $flags)
- {
- $this->initProtocol($path);
-
- $split = explode('://', $path);
- $path = strtolower($split[0]) . '://' . $split[1];
-
- if ($value = $this->getCacheStorage()->get($path)) {
- return $value;
- }
- $stat = $this->createStat($path, $flags);
- if (is_array($stat)) {
- $this->getCacheStorage()->set($path, $stat);
- }
- return $stat;
- }
-
- private function initProtocol($path)
- {
- $parts = explode('://', $path, 2);
- $this->protocol = $parts[0] ?: 's3';
- }
- private function createStat($path, $flags)
- {
- $this->initProtocol($path);
- $parts = $this->withPath($path);
- if (!$parts['Key']) {
- return $this->statDirectory($parts, $path, $flags);
- }
- return $this->boolCall(function () use ($parts, $path) {
- try {
- $result = $this->getClient()->headObject($parts);
- if (substr($parts['Key'], -1, 1) == '/' &&
- $result['ContentLength'] == 0
- ) {
-
-
- return $this->formatUrlStat($path);
- }
-
- return $this->formatUrlStat($result->toArray());
- } catch (S3Exception $e) {
-
-
- $result = $this->getClient()->listObjects([
- 'Bucket' => $parts['Bucket'],
- 'Prefix' => rtrim($parts['Key'], '/') . '/',
- 'MaxKeys' => 1
- ]);
- if (!$result['Contents'] && !$result['CommonPrefixes']) {
- throw new \Exception("File or directory not found: $path");
- }
- return $this->formatUrlStat($path);
- }
- }, $flags);
- }
- private function statDirectory($parts, $path, $flags)
- {
-
- $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
- if (!$parts['Bucket'] ||
- $this->getClient()->$method($parts['Bucket'])
- ) {
- return $this->formatUrlStat($path);
- }
- return $this->triggerError("File or directory not found: $path", $flags);
- }
-
- public function mkdir($path, $mode, $options)
- {
- $this->initProtocol($path);
- $params = $this->withPath($path);
- $this->clearCacheKey($path);
- if (!$params['Bucket']) {
- return false;
- }
- if (!isset($params['ACL'])) {
- $params['ACL'] = $this->determineAcl($mode);
- }
- return empty($params['Key'])
- ? $this->createBucket($path, $params)
- : $this->createSubfolder($path, $params);
- }
- public function rmdir($path, $options)
- {
- $this->initProtocol($path);
- $this->clearCacheKey($path);
- $params = $this->withPath($path);
- $client = $this->getClient();
- if (!$params['Bucket']) {
- return $this->triggerError('You must specify a bucket');
- }
- return $this->boolCall(function () use ($params, $path, $client) {
- if (!$params['Key']) {
- $client->deleteBucket(['Bucket' => $params['Bucket']]);
- return true;
- }
- return $this->deleteSubfolder($path, $params);
- });
- }
-
- public function dir_opendir($path, $options)
- {
- $this->initProtocol($path);
- $this->openedPath = $path;
- $params = $this->withPath($path);
- $delimiter = $this->getOption('delimiter');
-
- $filterFn = $this->getOption('listFilter');
- $op = ['Bucket' => $params['Bucket']];
- $this->openedBucket = $params['Bucket'];
- if ($delimiter === null) {
- $delimiter = '/';
- }
- if ($delimiter) {
- $op['Delimiter'] = $delimiter;
- }
- if ($params['Key']) {
- $params['Key'] = rtrim($params['Key'], $delimiter) . $delimiter;
- $op['Prefix'] = $params['Key'];
- }
- $this->openedBucketPrefix = $params['Key'];
-
-
- $this->objectIterator = \Aws\flatmap(
- $this->getClient()->getPaginator('ListObjects', $op),
- function (Result $result) use ($filterFn) {
- $contentsAndPrefixes = $result->search('[Contents[], CommonPrefixes[]][]');
-
- return array_filter(
- $contentsAndPrefixes,
- function ($key) use ($filterFn) {
- return (!$filterFn || call_user_func($filterFn, $key))
- && (!isset($key['Key']) || substr($key['Key'], -1, 1) !== '/');
- }
- );
- }
- );
- return true;
- }
-
- public function dir_closedir()
- {
- $this->objectIterator = null;
- gc_collect_cycles();
- return true;
- }
-
- public function dir_rewinddir()
- {
- return $this->boolCall(function() {
- $this->objectIterator = null;
- $this->dir_opendir($this->openedPath, null);
- return true;
- });
- }
-
- public function dir_readdir()
- {
-
- if (!$this->objectIterator->valid()) {
- return false;
- }
-
-
-
-
-
-
-
- $cur = $this->objectIterator->current();
- if (isset($cur['Prefix'])) {
-
-
- $result = rtrim($cur['Prefix'], '/');
- $key = $this->formatKey($result);
- $stat = $this->formatUrlStat($key);
- } else {
- $result = $cur['Key'];
- $key = $this->formatKey($cur['Key']);
- $stat = $this->formatUrlStat($cur);
- }
-
-
- $this->getCacheStorage()->set($key, $stat);
- $this->objectIterator->next();
-
- return $this->openedBucketPrefix
- ? substr($result, strlen($this->openedBucketPrefix))
- : $result;
- }
- private function formatKey($key)
- {
- $protocol = explode('://', $this->openedPath)[0];
- return "{$protocol}://{$this->openedBucket}/{$key}";
- }
-
- public function rename($path_from, $path_to)
- {
-
-
- $this->initProtocol($path_from);
- $partsFrom = $this->withPath($path_from);
- $partsTo = $this->withPath($path_to);
- $this->clearCacheKey($path_from);
- $this->clearCacheKey($path_to);
- if (!$partsFrom['Key'] || !$partsTo['Key']) {
- return $this->triggerError('The Amazon S3 stream wrapper only '
- . 'supports copying objects');
- }
- return $this->boolCall(function () use ($partsFrom, $partsTo) {
- $options = $this->getOptions(true);
-
-
- $this->getClient()->copy(
- $partsFrom['Bucket'],
- $partsFrom['Key'],
- $partsTo['Bucket'],
- $partsTo['Key'],
- isset($options['acl']) ? $options['acl'] : 'private',
- $options
- );
-
- $this->getClient()->deleteObject([
- 'Bucket' => $partsFrom['Bucket'],
- 'Key' => $partsFrom['Key']
- ] + $options);
- return true;
- });
- }
- public function stream_cast($cast_as)
- {
- return false;
- }
-
- private function validate($path, $mode)
- {
- $errors = [];
- if (!$this->getOption('Key')) {
- $errors[] = 'Cannot open a bucket. You must specify a path in the '
- . 'form of s3://bucket/key';
- }
- if (!in_array($mode, ['r', 'w', 'a', 'x'])) {
- $errors[] = "Mode not supported: {$mode}. "
- . "Use one 'r', 'w', 'a', or 'x'.";
- }
- if ($mode === 'x') {
- $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
- if ($this->getClient()->$method(
- $this->getOption('Bucket'),
- $this->getOption('Key'),
- $this->getOptions(true)
- )) {
- $errors[] = "{$path} already exists on Amazon S3";
- }
- }
- return $errors;
- }
-
- private function getOptions($removeContextData = false)
- {
-
- if ($this->context === null) {
- $options = [];
- } else {
- $options = stream_context_get_options($this->context);
- $options = isset($options[$this->protocol])
- ? $options[$this->protocol]
- : [];
- }
- $default = stream_context_get_options(stream_context_get_default());
- $default = isset($default[$this->protocol])
- ? $default[$this->protocol]
- : [];
- $result = $this->params + $options + $default;
- if ($removeContextData) {
- unset($result['client'], $result['seekable'], $result['cache']);
- }
- return $result;
- }
-
- private function getOption($name)
- {
- $options = $this->getOptions();
- return isset($options[$name]) ? $options[$name] : null;
- }
-
- private function getClient()
- {
- if (!$client = $this->getOption('client')) {
- throw new \RuntimeException('No client in stream context');
- }
- return $client;
- }
- private function getBucketKey($path)
- {
-
- $parts = explode('://', $path);
-
- $parts = explode('/', $parts[1], 2);
- return [
- 'Bucket' => $parts[0],
- 'Key' => isset($parts[1]) ? $parts[1] : null
- ];
- }
-
- private function withPath($path)
- {
- $params = $this->getOptions(true);
- return $this->getBucketKey($path) + $params;
- }
- private function openReadStream()
- {
- $client = $this->getClient();
- $command = $client->getCommand('GetObject', $this->getOptions(true));
- $command['@http']['stream'] = true;
- $result = $client->execute($command);
- $this->size = $result['ContentLength'];
- $this->body = $result['Body'];
-
- if ($this->getOption('seekable') && !$this->body->isSeekable()) {
- $this->body = new CachingStream($this->body);
- }
- return true;
- }
- private function openWriteStream()
- {
- $this->body = new Stream(fopen('php://temp', 'r+'));
- return true;
- }
- private function openAppendStream()
- {
- try {
-
- $client = $this->getClient();
- $this->body = $client->getObject($this->getOptions(true))['Body'];
- $this->body->seek(0, SEEK_END);
- return true;
- } catch (S3Exception $e) {
-
- return $this->openWriteStream();
- }
- }
-
- private function triggerError($errors, $flags = null)
- {
-
- if ($flags & STREAM_URL_STAT_QUIET) {
- return $flags & STREAM_URL_STAT_LINK
-
- ? $this->formatUrlStat(false)
- : false;
- }
-
- trigger_error(implode("\n", (array) $errors), E_USER_WARNING);
- return false;
- }
-
- private function formatUrlStat($result = null)
- {
- $stat = $this->getStatTemplate();
- switch (gettype($result)) {
- case 'NULL':
- case 'string':
-
- $stat['mode'] = $stat[2] = 0040777;
- break;
- case 'array':
-
- $stat['mode'] = $stat[2] = 0100777;
-
- if (isset($result['ContentLength'])) {
- $stat['size'] = $stat[7] = $result['ContentLength'];
- } elseif (isset($result['Size'])) {
- $stat['size'] = $stat[7] = $result['Size'];
- }
- if (isset($result['LastModified'])) {
-
- $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10]
- = strtotime($result['LastModified']);
- }
- }
- return $stat;
- }
-
- private function createBucket($path, array $params)
- {
- $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist';
- if ($this->getClient()->$method($params['Bucket'])) {
- return $this->triggerError("Bucket already exists: {$path}");
- }
- unset($params['ACL']);
- return $this->boolCall(function () use ($params, $path) {
- $this->getClient()->createBucket($params);
- $this->clearCacheKey($path);
- return true;
- });
- }
-
- private function createSubfolder($path, array $params)
- {
-
- $params['Key'] = rtrim($params['Key'], '/') . '/';
- $params['Body'] = '';
-
- $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist';
- if ($this->getClient()->$method(
- $params['Bucket'],
- $params['Key']
- )) {
- return $this->triggerError("Subfolder already exists: {$path}");
- }
- return $this->boolCall(function () use ($params, $path) {
- $this->getClient()->putObject($params);
- $this->clearCacheKey($path);
- return true;
- });
- }
-
- private function deleteSubfolder($path, $params)
- {
-
- $prefix = rtrim($params['Key'], '/') . '/';
- $result = $this->getClient()->listObjects([
- 'Bucket' => $params['Bucket'],
- 'Prefix' => $prefix,
- 'MaxKeys' => 1
- ]);
-
- if ($contents = $result['Contents']) {
- return (count($contents) > 1 || $contents[0]['Key'] != $prefix)
- ? $this->triggerError('Subfolder is not empty')
- : $this->unlink(rtrim($path, '/') . '/');
- }
- return $result['CommonPrefixes']
- ? $this->triggerError('Subfolder contains nested folders')
- : true;
- }
-
- private function determineAcl($mode)
- {
- switch (substr(decoct($mode), 0, 1)) {
- case '7': return 'public-read';
- case '6': return 'authenticated-read';
- default: return 'private';
- }
- }
-
- private function getStatTemplate()
- {
- return [
- 0 => 0, 'dev' => 0,
- 1 => 0, 'ino' => 0,
- 2 => 0, 'mode' => 0,
- 3 => 0, 'nlink' => 0,
- 4 => 0, 'uid' => 0,
- 5 => 0, 'gid' => 0,
- 6 => -1, 'rdev' => -1,
- 7 => 0, 'size' => 0,
- 8 => 0, 'atime' => 0,
- 9 => 0, 'mtime' => 0,
- 10 => 0, 'ctime' => 0,
- 11 => -1, 'blksize' => -1,
- 12 => -1, 'blocks' => -1,
- ];
- }
-
- private function boolCall(callable $fn, $flags = null)
- {
- try {
- return $fn();
- } catch (\Exception $e) {
- return $this->triggerError($e->getMessage(), $flags);
- }
- }
-
- private function getCacheStorage()
- {
- if (!$this->cache) {
- $this->cache = $this->getOption('cache') ?: new LruArrayCache();
- }
- return $this->cache;
- }
-
- private function clearCacheKey($key)
- {
- clearstatcache(true, $key);
- $this->getCacheStorage()->remove($key);
- }
-
- private function getSize()
- {
- $size = $this->body->getSize();
- return !empty($size) ? $size : $this->size;
- }
- }
|