summaryrefslogtreecommitdiffstats
path: root/apps/files_external/3rdparty/aws-sdk-php/Aws/S3/StreamWrapper.php
diff options
context:
space:
mode:
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.php266
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';
+ }
}