diff options
Diffstat (limited to 'apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php')
-rw-r--r-- | apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php | 266 |
1 files changed, 196 insertions, 70 deletions
diff --git a/apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php b/apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php index 86c2610b63d..3bb85aae6df 100644 --- a/apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php +++ b/apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php @@ -20,9 +20,10 @@ use Aws\Common\Exception\RuntimeException; use Aws\S3\Exception\S3Exception; use Aws\S3\Exception\NoSuchKeyException; use Aws\S3\Iterator\ListObjectsIterator; -use Guzzle\Http\QueryString; use Guzzle\Http\EntityBody; use Guzzle\Http\CachingEntityBody; +use Guzzle\Http\Mimetypes; +use Guzzle\Iterator\FilterIterator; use Guzzle\Stream\PhpStreamRequestFactory; use Guzzle\Service\Command\CommandInterface; @@ -71,7 +72,6 @@ use Guzzle\Service\Command\CommandInterface; * Stream context options: * * - "seekable": Set to true to create a seekable "r" (read only) stream by using a php://temp stream buffer - * - "throw_exceptions": Set to true to throw exceptions instead of trigger_errors * - For "unlink" only: Any option that can be passed to the DeleteObject operation */ class StreamWrapper @@ -132,7 +132,7 @@ class StreamWrapper stream_wrapper_unregister('s3'); } - stream_wrapper_register('s3', __CLASS__, STREAM_IS_URL); + stream_wrapper_register('s3', get_called_class(), STREAM_IS_URL); self::$client = $client; } @@ -172,8 +172,8 @@ class StreamWrapper } // When using mode "x" validate if the file exists before attempting to read - if ($mode == 'x' && !self::$client->doesObjectExist($params['Bucket'], $params['Key'], $this->getOptions())) { - $errors[] = "{$path} does not exist on Amazon S3"; + if ($mode == 'x' && self::$client->doesObjectExist($params['Bucket'], $params['Key'], $this->getOptions())) { + $errors[] = "{$path} already exists on Amazon S3"; } if (!$errors) { @@ -210,6 +210,14 @@ class StreamWrapper $params = $this->params; $params['Body'] = $this->body; + // Attempt to guess the ContentType of the upload based on the + // file extension of the key + if (!isset($params['ContentType']) && + ($type = Mimetypes::getInstance()->fromFilename($params['Key'])) + ) { + $params['ContentType'] = $type; + } + try { self::$client->putObject($params); return true; @@ -314,25 +322,30 @@ class StreamWrapper $parts = $this->getParams($path); - // Stat a bucket or just s3:// - if (!$parts['Key'] && (!$parts['Bucket'] || self::$client->doesBucketExist($parts['Bucket']))) { - return $this->formatUrlStat($path); - } - - // You must pass either a bucket or a bucket + key if (!$parts['Key']) { - return $this->triggerError("File or directory not found: {$path}", $flags); + // Stat "directories": buckets, or "s3://" + if (!$parts['Bucket'] || self::$client->doesBucketExist($parts['Bucket'])) { + return $this->formatUrlStat($path); + } else { + return $this->triggerError("File or directory not found: {$path}", $flags); + } } try { try { - // Attempt to stat and cache regular object - return $this->formatUrlStat(self::$client->headObject($parts)->toArray()); + $result = self::$client->headObject($parts)->toArray(); + if (substr($parts['Key'], -1, 1) == '/' && $result['ContentLength'] == 0) { + // Return as if it is a bucket to account for console bucket objects (e.g., zero-byte object "foo/") + return $this->formatUrlStat($path); + } else { + // Attempt to stat and cache regular object + return $this->formatUrlStat($result); + } } catch (NoSuchKeyException $e) { // Maybe this isn't an actual key, but a prefix. Do a prefix listing of objects to determine. $result = self::$client->listObjects(array( 'Bucket' => $parts['Bucket'], - 'Prefix' => $parts['Key'], + 'Prefix' => rtrim($parts['Key'], '/') . '/', 'MaxKeys' => 1 )); if (!$result['Contents'] && !$result['CommonPrefixes']) { @@ -352,7 +365,7 @@ class StreamWrapper * @param string $path Directory which should be created. * @param int $mode Permissions. 700-range permissions map to ACL_PUBLIC. 600-range permissions map to * ACL_AUTH_READ. All other permissions map to ACL_PRIVATE. Expects octal form. - * @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE. (unused) + * @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE. * * @return bool * @link http://www.php.net/manual/en/streamwrapper.mkdir.php @@ -360,28 +373,17 @@ class StreamWrapper public function mkdir($path, $mode, $options) { $params = $this->getParams($path); - $this->clearStatInfo($path); - - if (!$params['Bucket'] || $params['Key']) { + if (!$params['Bucket']) { return false; } - try { - if (!isset($params['ACL'])) { - $mode = decoct($mode); - if ($mode >= 700 and $mode <= 799) { - $params['ACL'] = 'public-read'; - } elseif ($mode >= 600 && $mode <= 699) { - $params['ACL'] = 'authenticated-read'; - } else { - $params['ACL'] = 'private'; - } - } - self::$client->createBucket($params); - return true; - } catch (\Exception $e) { - return $this->triggerError($e->getMessage()); + if (!isset($params['ACL'])) { + $params['ACL'] = $this->determineAcl($mode); } + + return !isset($params['Key']) || $params['Key'] === '/' + ? $this->createBucket($path, $params) + : $this->createPseudoDirectory($path, $params); } /** @@ -397,14 +399,40 @@ class StreamWrapper $params = $this->getParams($path); if (!$params['Bucket']) { return $this->triggerError('You cannot delete s3://. Please specify a bucket.'); - } elseif ($params['Key']) { - return $this->triggerError('rmdir() only supports bucket deletion'); } try { - self::$client->deleteBucket(array('Bucket' => $params['Bucket'])); - $this->clearStatInfo($path); - return true; + + if (!$params['Key']) { + self::$client->deleteBucket(array('Bucket' => $params['Bucket'])); + $this->clearStatInfo($path); + return true; + } + + // Use a key that adds a trailing slash if needed. + $prefix = rtrim($params['Key'], '/') . '/'; + + $result = self::$client->listObjects(array( + 'Bucket' => $params['Bucket'], + 'Prefix' => $prefix, + 'MaxKeys' => 1 + )); + + // Check if the bucket contains keys other than the placeholder + if ($result['Contents']) { + foreach ($result['Contents'] as $key) { + if ($key['Key'] == $prefix) { + continue; + } + return $this->triggerError('Psuedo folder is not empty'); + } + return $this->unlink(rtrim($path, '/') . '/'); + } + + return $result['CommonPrefixes'] + ? $this->triggerError('Pseudo folder contains nested folders') + : true; + } catch (\Exception $e) { return $this->triggerError($e->getMessage()); } @@ -413,6 +441,11 @@ class StreamWrapper /** * Support for opendir(). * + * The opendir() method of the Amazon S3 stream wrapper supports a stream + * context option of "listFilter". listFilter must be a callable that + * accepts an associative array of object data and returns true if the + * object should be yielded when iterating the keys in a bucket. + * * @param string $path The path to the directory (e.g. "s3://dir[</prefix>]") * @param string $options Whether or not to enforce safe_mode (0x04). Unused. * @@ -425,14 +458,14 @@ class StreamWrapper $this->clearStatInfo(); $params = $this->getParams($path); $delimiter = $this->getOption('delimiter'); + $filterFn = $this->getOption('listFilter'); if ($delimiter === null) { $delimiter = '/'; } if ($params['Key']) { - $suffix = $delimiter ?: '/'; - $params['Key'] = rtrim($params['Key'], $suffix) . $suffix; + $params['Key'] = rtrim($params['Key'], $delimiter) . $delimiter; } $this->openedBucket = $params['Bucket']; @@ -443,11 +476,22 @@ class StreamWrapper $operationParams['Delimiter'] = $delimiter; } - $this->objectIterator = self::$client->getIterator('ListObjects', $operationParams, array( + $objectIterator = self::$client->getIterator('ListObjects', $operationParams, array( 'return_prefixes' => true, 'sort_results' => true )); + // Filter our "/" keys added by the console as directories, and ensure + // that if a filter function is provided that it passes the filter. + $this->objectIterator = new FilterIterator( + $objectIterator, + function ($key) use ($filterFn) { + // Each yielded results can contain a "Key" or "Prefix" + return (!$filterFn || call_user_func($filterFn, $key)) && + (!isset($key['Key']) || substr($key['Key'], -1, 1) !== '/'); + } + ); + $this->objectIterator->next(); return true; @@ -487,26 +531,32 @@ class StreamWrapper */ public function dir_readdir() { - $result = false; - if ($this->objectIterator->valid()) { - $current = $this->objectIterator->current(); - if (isset($current['Prefix'])) { - // Include "directories" - $result = str_replace($this->openedBucketPrefix, '', $current['Prefix']); - $key = "s3://{$this->openedBucket}/{$current['Prefix']}"; - $stat = $this->formatUrlStat($current['Prefix']); - } else { - // Remove the prefix from the result to emulate other stream wrappers - $result = str_replace($this->openedBucketPrefix, '', $current['Key']); - $key = "s3://{$this->openedBucket}/{$current['Key']}"; - $stat = $this->formatUrlStat($current); - } + // Skip empty result keys + if (!$this->objectIterator->valid()) { + return false; + } - // Cache the object data for quick url_stat lookups used with RecursiveDirectoryIterator - self::$nextStat = array($key => $stat); - $this->objectIterator->next(); + $current = $this->objectIterator->current(); + if (isset($current['Prefix'])) { + // Include "directories". Be sure to strip a trailing "/" + // on prefixes. + $prefix = rtrim($current['Prefix'], '/'); + $result = str_replace($this->openedBucketPrefix, '', $prefix); + $key = "s3://{$this->openedBucket}/{$prefix}"; + $stat = $this->formatUrlStat($prefix); + } else { + // Remove the prefix from the result to emulate other + // stream wrappers. + $result = str_replace($this->openedBucketPrefix, '', $current['Key']); + $key = "s3://{$this->openedBucket}/{$current['Key']}"; + $stat = $this->formatUrlStat($current); } + // Cache the object data for quick url_stat lookups used with + // RecursiveDirectoryIterator. + self::$nextStat = array($key => $stat); + $this->objectIterator->next(); + return $result; } @@ -602,7 +652,6 @@ class StreamWrapper $params = $this->getOptions(); unset($params['seekable']); - unset($params['throw_exceptions']); return array( 'Bucket' => $parts[0], @@ -695,14 +744,19 @@ class StreamWrapper */ protected function triggerError($errors, $flags = null) { - if ($flags != STREAM_URL_STAT_QUIET) { - if ($this->getOption('throw_exceptions')) { - throw new RuntimeException(implode("\n", (array) $errors)); - } else { - trigger_error(implode("\n", (array) $errors), E_USER_WARNING); - } + if ($flags & STREAM_URL_STAT_QUIET) { + // This is triggered with things like file_exists() + + if ($flags & STREAM_URL_STAT_LINK) { + // This is triggered for things like is_link() + return $this->formatUrlStat(false); + } + return false; } + // This is triggered when doing things like lstat() or stat() + trigger_error(implode("\n", (array) $errors), E_USER_WARNING); + return false; } @@ -732,19 +786,18 @@ class StreamWrapper ); $stat = $statTemplate; + $type = gettype($result); // Determine what type of data is being cached - if (!$result || is_string($result)) { + if ($type == 'NULL' || $type == 'string') { // Directory with 0777 access - see "man 2 stat". $stat['mode'] = $stat[2] = 0040777; - } elseif (is_array($result) && isset($result['LastModified'])) { + } elseif ($type == 'array' && isset($result['LastModified'])) { // ListObjects or HeadObject result $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10] = strtotime($result['LastModified']); $stat['size'] = $stat[7] = (isset($result['ContentLength']) ? $result['ContentLength'] : $result['Size']); // Regular file with 0777 access - see "man 2 stat". $stat['mode'] = $stat[2] = 0100777; - } else { - $stat['mode'] = $stat[2] = 0100777; } return $stat; @@ -762,4 +815,77 @@ class StreamWrapper clearstatcache(true, $path); } } + + /** + * Creates a bucket for the given parameters. + * + * @param string $path Stream wrapper path + * @param array $params A result of StreamWrapper::getParams() + * + * @return bool Returns true on success or false on failure + */ + private function createBucket($path, array $params) + { + if (self::$client->doesBucketExist($params['Bucket'])) { + return $this->triggerError("Directory already exists: {$path}"); + } + + try { + self::$client->createBucket($params); + $this->clearStatInfo($path); + return true; + } catch (\Exception $e) { + return $this->triggerError($e->getMessage()); + } + } + + /** + * Creates a pseudo-folder by creating an empty "/" suffixed key + * + * @param string $path Stream wrapper path + * @param array $params A result of StreamWrapper::getParams() + * + * @return bool + */ + private function createPseudoDirectory($path, array $params) + { + // Ensure the path ends in "/" and the body is empty. + $params['Key'] = rtrim($params['Key'], '/') . '/'; + $params['Body'] = ''; + + // Fail if this pseudo directory key already exists + if (self::$client->doesObjectExist($params['Bucket'], $params['Key'])) { + return $this->triggerError("Directory already exists: {$path}"); + } + + try { + self::$client->putObject($params); + $this->clearStatInfo($path); + return true; + } catch (\Exception $e) { + return $this->triggerError($e->getMessage()); + } + } + + /** + * Determine the most appropriate ACL based on a file mode. + * + * @param int $mode File mode + * + * @return string + */ + private function determineAcl($mode) + { + $mode = decoct($mode); + + if ($mode >= 700 && $mode <= 799) { + return 'public-read'; + } + + if ($mode >= 600 && $mode <= 699) { + return 'authenticated-read'; + } + + return 'private'; + } } |