class S3fsStreamWrapper in S3 File System 7.3
Same name and namespace in other branches
- 7 S3fsStreamWrapper.inc \S3fsStreamWrapper
- 7.2 S3fsStreamWrapper.inc \S3fsStreamWrapper
The stream wrapper class.
In the docs for this class, anywhere you see "<scheme>", it can mean either "s3" or "public", depending on which stream is currently being serviced.
Hierarchy
- class \S3fsStreamWrapper extends \Aws\S3\StreamWrapper implements DrupalStreamWrapperInterface
Expanded class hierarchy of S3fsStreamWrapper
3 string references to 'S3fsStreamWrapper'
- s3fs.test in tests/
s3fs.test - s3fs_stream_wrappers in ./
s3fs.module - Implements hook_stream_wrappers().
- s3fs_stream_wrappers_alter in ./
s3fs.module - Implements hook_stream_wrappers_alter().
File
- ./
S3fsStreamWrapper.inc, line 28 - Drupal stream wrapper implementation for S3 File System.
View source
class S3fsStreamWrapper extends StreamWrapper implements DrupalStreamWrapperInterface {
/**
* Stream context (this is set by PHP when a context is used).
*
* @var resource
*/
public $context;
/**
* Hash of opened stream parameters.
*
* @var array
*/
private $params = array();
/**
* Module configuration for stream.
*
* @var array
*/
private $config = array();
/**
* Mode in which the stream was opened.
*
* @var string
*/
private $mode;
/**
* Instance URI referenced as "<scheme>://key".
*
* @var string
*/
protected $uri = NULL;
/**
* The AWS SDK for PHP S3Client object.
*
* @var Aws\S3\S3Client
*/
protected $s3 = NULL;
/**
* The opened protocol (e.g., "s3").
*
* @var string
*/
private $protocol = 's3';
/**
* Domain we use to access files over http.
*
* @var string
*/
protected $domain = NULL;
/**
* Directory listing used by the dir_* methods.
*
* @var array
*/
protected $dir = NULL;
/**
* Map for files that should be delivered with a torrent URL.
*
* @var array
*/
protected $torrents = array();
/**
* Files that the user has said must be downloaded, rather than viewed.
*
* @var array
*/
protected $saveas = array();
/**
* Files which should be created with URLs that eventually time out.
*
* @var array
*/
protected $presignedURLs = array();
/**
* Default map for determining file mime types.
*
* @var array
*/
protected static $mimeTypeMapping = NULL;
/**
* Indicates the current error state in the wrapper.
*
* @var bool
*/
protected $_error_state = FALSE;
/**
* Stream wrapper constructor.
*
* Creates the Aws\S3\S3Client client object and activates the options
* specified on the S3 File System Settings page.
*/
public function __construct() {
// Since S3fsStreamWrapper is always constructed with the same inputs (the
// file uri is not part of construction), we store the constructed settings
// statically. This is important for performance because the way Drupal's
// APIs are used causes stream wrappers to be frequently re-constructed.
// Get the S3 client object and register the stream wrapper again so it is
// configured as needed.
$settings =& drupal_static('S3fsStreamWrapper_constructed_settings');
if ($settings !== NULL) {
$this->config = $settings['config'];
$this->domain = $settings['domain'];
$this->torrents = $settings['torrents'];
$this->presignedURLs = $settings['presignedURLs'];
$this->saveas = $settings['saveas'];
$this->s3 = _s3fs_get_amazons3_client($this->config);
$this
->register($this->s3);
return;
}
// Begin construction if not cached.
$this->config = _s3fs_get_config();
$this->s3 = _s3fs_get_amazons3_client($this->config);
$this
->register($this->s3);
$this->context = stream_context_get_default();
stream_context_set_option($this->context, 's3', 'seekable', TRUE);
// Check if bucket is configured.
if (empty($this->config['bucket'])) {
$msg = t('Your AmazonS3 bucket name is not configured. Please visit the !settings_page.', array(
'!settings_page' => l(t('configuration page'), '/admin/config/media/s3fs/settings'),
));
watchdog('S3 File System', $msg, array(), WATCHDOG_ERROR);
throw new Exception($msg);
}
// Always use HTTPS when the page is being served via HTTPS, to avoid
// complaints from the browser about insecure content.
global $is_https;
if ($is_https) {
// We change the config itself, rather than simply using $is_https in
// the following if condition, because $this->config['use_https'] gets
// used again later.
$this->config['use_https'] = TRUE;
}
$scheme = !empty($this->config['use_https']) ? 'https' : 'http';
// CNAME support for customizing S3 URLs.
// If use_cname is not enabled, file URLs do not use $this->domain.
if (!empty($this->config['use_cname']) && !empty($this->config['domain'])) {
$domain = check_url($this->config['domain']);
if ($domain) {
// If domain is set to a root-relative path, add the hostname back in.
if (strpos($domain, '/') === 0) {
$domain = $_SERVER['HTTP_HOST'] . $domain;
}
$this->domain = "{$scheme}://{$domain}";
}
else {
// Due to the config form's validation, this shouldn't ever happen.
throw new Exception(t('The "Use a CNAME" option is enabled, but no CDN Domain Name has been set.'));
}
}
// Convert the torrents string to an array.
if (!empty($this->config['torrents'])) {
foreach (explode("\n", $this->config['torrents']) as $line) {
$blob = trim($line);
if ($blob) {
$this->torrents[] = $blob;
}
}
}
// Convert the presigned URLs string to an associative array like
// array(blob => timeout).
if (!empty($this->config['presigned_urls'])) {
foreach (explode("\n", $this->config['presigned_urls']) as $line) {
$blob = trim($line);
if ($blob) {
$matches = array();
if (preg_match('/(.*)\\|(.*)/', $blob, $matches)) {
$blob = $matches[2];
$timeout = $matches[1];
$this->presignedURLs[$blob] = $timeout;
}
else {
$this->presignedURLs[$blob] = 60;
}
}
}
}
// Convert the forced save-as string to an array.
if (!empty($this->config['saveas'])) {
foreach (explode("\n", $this->config['saveas']) as $line) {
$blob = trim($line);
if ($blob) {
$this->saveas[] = $blob;
}
}
}
// Save all the work we just did, so that subsequent S3fsStreamWrapper
// constructions don't have to repeat it.
$settings['config'] = $this->config;
$settings['domain'] = $this->domain;
$settings['torrents'] = $this->torrents;
$settings['presignedURLs'] = $this->presignedURLs;
$settings['saveas'] = $this->saveas;
}
/**
* Ensure the S3 protocol is registered to this class and not parents.
*
* @param \Aws\S3\S3ClientInterface $client
* @param string $protocol
* @param \Aws\CacheInterface|null $cache
*/
public static function register(S3ClientInterface $client, $protocol = 's3', CacheInterface $cache = null) {
parent::register($client, $protocol, $cache);
}
/***************************************************************************
DrupalStreamWrapperInterface Implementations
***************************************************************************/
public static function getMimeType($uri, $mapping = NULL) {
// Load the default mime type map.
if (!isset(self::$mimeTypeMapping)) {
include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
self::$mimeTypeMapping = file_mimetype_mapping();
}
// If a mapping wasn't specified, use the default map.
if ($mapping == NULL) {
$mapping = self::$mimeTypeMapping;
}
$extension = '';
$file_parts = explode('.', drupal_basename($uri));
// Remove the first part: a full filename should not match an extension.
array_shift($file_parts);
// Iterate over the file parts, trying to find a match.
// For my.awesome.image.jpeg, we try:
// - jpeg
// - image.jpeg
// - awesome.image.jpeg
while ($additional_part = array_pop($file_parts)) {
$extension = strtolower($additional_part . ($extension ? '.' . $extension : ''));
if (isset($mapping['extensions'][$extension])) {
return $mapping['mimetypes'][$mapping['extensions'][$extension]];
}
}
// No mime type matches, so return the default.
return 'application/octet-stream';
}
/**
* Sets the stream resource URI. URIs are formatted as "<scheme>://filepath".
*
* @param string $uri
* The URI that should be used for this instance.
*/
public function setUri($uri) {
$this->uri = $uri;
}
/**
* Returns the stream resource URI, which looks like "<scheme>://filepath".
*
* @return string
* The current URI of the instance.
*/
public function getUri() {
return $this->uri;
}
/**
* Returns a web accessible URL for the resource.
*
* The format of the returned URL will be different depending on how the S3
* integration has been configured on the S3 File System admin page.
*
* @param bool $no_redirect
* A custom parameter for internal use by s3fs.
*
* @return string
* A web accessible URL for the resource.
*/
public function getExternalUrl() {
// In case we're on Windows, replace backslashes with forward-slashes.
// Note that $uri is the unaltered value of the File's URI, while
// $s3_key may be changed at various points to account for implementation
// details on the S3 side (e.g. root_folder, s3fs_public_folder).
$s3_key = str_replace('\\', '/', file_uri_target($this->uri));
// If this is a private:// file, it must be served through the
// system/files/$uri URL, which allows Drupal to restrict access
// based on who's logged in.
if (file_uri_scheme($this->uri) == 'private') {
return url("system/files/{$s3_key}", array(
'absolute' => TRUE,
));
}
// When generating an image derivative URL, e.g. styles/thumbnail/blah.jpg,
// if the file doesn't exist, provide a URL to s3fs's special version of
// image_style_deliver(), which will create the derivative when that URL
// gets requested.
$uri_parts = explode('/', $s3_key);
if ($uri_parts[0] == 'styles' && substr($s3_key, -4) != '.css') {
$metadata = $this
->_s3fs_get_object($this->uri);
$is_zero_byte = !isset($metadata['filesize']) || $metadata['filesize'] == 0;
if (!$metadata || $is_zero_byte) {
if ($is_zero_byte) {
$this
->unlink($this->uri);
}
// The style delivery path looks like: s3/files/styles/thumbnail/...
// And $uri_parts looks like array('styles', 'thumbnail', ...),
// so just prepend s3/files/.
array_unshift($uri_parts, 's3', 'files');
return url(implode('/', $uri_parts), array(
'absolute' => TRUE,
));
}
unset($metadata);
}
// Deal with public:// files.
if (file_uri_scheme($this->uri) == 'public') {
// Rewrite all css/js file paths unless the user has told us not to.
if (empty($this->config['no_rewrite_cssjs'])) {
if (substr($s3_key, -4) == '.css') {
// Send requests for public CSS files to /s3fs-css/path/to/file.css.
// Users must set that path up in the webserver config as a proxy into
// their S3 bucket's s3fs_public_folder.
return "{$GLOBALS['base_url']}/s3fs-css/" . drupal_encode_path($s3_key);
}
elseif (substr($s3_key, -3) == '.js') {
// Send requests for public JS files to /s3fs-js/path/to/file.js.
// Like with CSS, the user must set up that path as a proxy.
return "{$GLOBALS['base_url']}/s3fs-js/" . drupal_encode_path($s3_key);
}
}
// public:// files are stored in S3 inside the s3fs_public_folder.
$public_folder = !empty($this->config['public_folder']) ? $this->config['public_folder'] : 's3fs-public';
// If domain root is not set, or the value is set to map to the root
// folder, include the public folder.
if ($this->config['domain_root'] == 'none' || $this->config['domain_root'] == 'root') {
$s3_key = "{$public_folder}/{$s3_key}";
}
}
// Set up the URL settings as specified in our settings page.
$url_settings = array(
'torrent' => FALSE,
'presigned_url' => FALSE,
'timeout' => 60,
'forced_saveas' => FALSE,
'api_args' => array(
'Scheme' => !empty($this->config['use_https']) ? 'https' : 'http',
),
'custom_GET_args' => array(),
);
// Presigned URLs.
foreach ($this->presignedURLs as $blob => $timeout) {
// ^ is used as the delimeter because it's an illegal character in URLs.
if (preg_match("^{$blob}^", $s3_key)) {
$url_settings['presigned_url'] = TRUE;
$url_settings['timeout'] = $timeout;
break;
}
}
// Forced Save As.
foreach ($this->saveas as $blob) {
if (preg_match("^{$blob}^", $s3_key)) {
$filename = drupal_basename($s3_key);
$url_settings['api_args']['ResponseContentDisposition'] = "attachment; filename=\"{$filename}\"";
$url_settings['forced_saveas'] = TRUE;
break;
}
}
// Allow other modules to change the URL settings.
drupal_alter('s3fs_url_settings', $url_settings, $s3_key);
// If a root folder has been set, and domain root is not set, prepend
// the root folder to the $s3_key at this time.
if (!empty($this->config['root_folder']) && $this->config['domain_root'] == 'none') {
$s3_key = "{$this->config['root_folder']}/{$s3_key}";
}
if (empty($this->config['use_cname'])) {
// We're not using a CNAME, so we ask S3 for the URL.
$expires = NULL;
if ($url_settings['presigned_url']) {
$expires = "+{$url_settings['timeout']} seconds";
}
else {
// Due to Amazon's security policies (see Request Parameters section @
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html),
// only signed requests can use request parameters.
// Thus, we must provide an expiry time for any URLs which specify
// Response* API args. Currently, this only includes "Forced Save As".
foreach ($url_settings['api_args'] as $key => $arg) {
if (strpos($key, 'Response') === 0) {
$expires = "+10 years";
break;
}
}
}
if ($url_settings['presigned_url']) {
$cmd = $this->s3
->getCommand('GetObject', array(
'Bucket' => $this->config['bucket'],
'Key' => $s3_key,
));
$external_url = (string) $this->s3
->createPresignedRequest($cmd, $expires)
->getUri();
}
else {
$external_url = $this->s3
->getObjectUrl($this->config['bucket'], $s3_key);
}
}
else {
// We are using a CNAME, so we need to manually construct the URL.
$external_url = "{$this->domain}/{$s3_key}";
}
// If this file is versioned, append the version number as a GET arg to
// ensure that browser caches will be bypassed upon version changes.
$meta = $this
->_read_cache($this->uri);
if (!empty($meta['version']) && !empty($this->config['use_versioning'])) {
$external_url = $this
->_append_get_arg($external_url, $meta['version']);
}
// Torrents can only be created for publicly-accessible files:
// https://forums.aws.amazon.com/thread.jspa?threadID=140949
// So Forced SaveAs and Presigned URLs cannot be served as torrents.
if (!$url_settings['forced_saveas'] && !$url_settings['presigned_url']) {
foreach ($this->torrents as $blob) {
if (preg_match("^{$blob}^", $s3_key)) {
// You get a torrent URL by adding a "torrent" GET arg.
$external_url = $this
->_append_get_arg($external_url, 'torrent');
break;
}
}
}
// If another module added a 'custom_GET_args' array to the url settings, process it here.
if (!empty($url_settings['custom_GET_args'])) {
foreach ($url_settings['custom_GET_args'] as $name => $value) {
$external_url = $this
->_append_get_arg($external_url, $name, $value);
}
}
return $external_url;
}
/**
* Gets the path that the wrapper is responsible for.
*
* This function isn't part of DrupalStreamWrapperInterface, but the rest
* of Drupal calls it as if it were, so we need to define it.
*
* @return string
* The empty string. Since this is a remote stream wrapper,
* it has no directory path.
*/
public function getDirectoryPath() {
return '';
}
/***************************************************************************
Public Functions for External Use of the Wrapper
***************************************************************************/
/**
* Wait for the specified file to exist in the bucket.
*
* @param string $uri
* The uri of the file.
*
* @return bool
* Returns TRUE once the waiting finishes, or FALSE if the file does not
* begin to exist within 10 seconds.
*/
public function waitUntilFileExists($uri) {
// Retry ten times, once every second.
$params = $this
->getCommandParams($uri);
$params['@waiter'] = array(
'delay' => 1,
'maxAttempts' => 10,
);
try {
$this->s3
->waitUntil('ObjectExists', $params);
return TRUE;
} catch (S3Exception $e) {
watchdog_exception('S3FS', $e);
return FALSE;
}
}
/**
* Write the file at the given uri into the metadata cache.
*
* This function is public so that other code can upload files to S3 and
* then have us write the correct metadata into our cache.
*/
public function writeUriToCache($uri) {
if ($this
->waitUntilFileExists($uri)) {
$metadata = $this
->_get_metadata_from_s3($uri);
$this
->_write_cache($metadata);
clearstatcache(TRUE, $uri);
}
}
/***************************************************************************
PHP Stream Wrapper Implementations
***************************************************************************/
/**
* This wrapper doesn't support file permissions.
*
* @param int $mode
* The file's desired permissions in octal. Consult PHP chmod() documentation
* for more information.
*
* @return bool
* Always returns TRUE.
*/
public function chmod($mode) {
$octal_mode = decoct($mode);
return TRUE;
}
/**
* This wrapper does not support realpath().
*
* @return bool
* Always returns FALSE.
*/
public function realpath() {
return FALSE;
}
/**
* Gets the name of the parent directory of a given path.
*
* This method is usually accessed through drupal_dirname(), which wraps
* around the normal PHP dirname() function, since it doesn't support stream
* wrappers.
*
* @param string $uri
* An optional URI.
*
* @return string
* The directory name, or FALSE if not applicable.
*
* @see drupal_dirname()
*/
public function dirname($uri = NULL) {
if (!isset($uri)) {
$uri = $this->uri;
}
$scheme = file_uri_scheme($uri);
$dirname = dirname(file_uri_target($uri));
// When the dirname() call above is given '$scheme://', it returns '.'.
// But '$scheme://.' is an invalid uri, so we return "$scheme://" instead.
if ($dirname == '.') {
$dirname = '';
}
return "{$scheme}://{$dirname}";
}
/**
* Support for fopen(), file_get_contents(), file_put_contents() etc.
*
* @param string $uri
* The URI of the file to open.
* @param string $mode
* The file mode. Only 'r', 'w', 'a', and 'x' are supported.
* @param int $options
* A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
* @param string $opened_path
* An OUT parameter populated with the path which was opened.
* This wrapper does not support this parameter.
*
* @return bool
* TRUE if file was opened successfully. Otherwise, FALSE.
*
* @see http://php.net/manual/en/streamwrapper.stream-open.php
*/
public function stream_open($uri, $mode, $options, &$opened_path) {
$this
->setUri($uri);
$converted = $this
->convertUriToKeyedPath($uri);
return parent::stream_open($converted, $mode, $options, $opened_path);
}
/**
* This wrapper does not support flock().
*
* @return bool
* Always Returns FALSE.
*
* @see http://php.net/manual/en/streamwrapper.stream-lock.php
*/
public function stream_lock($operation) {
return FALSE;
}
public function stream_metadata($path, $option, $value) {
return FALSE;
}
/**
* Support for fflush(). Flush current cached stream data to a file in S3.
*
* @return bool
* TRUE if data was successfully stored in S3.
*
* @see http://php.net/manual/en/streamwrapper.stream-flush.php
*/
public function stream_flush() {
// Prepare upload parameters.
$options = $this
->getOptions();
$options[$this->protocol]['ContentType'] = $this
->getMimeType($this->uri);
// All non-private files uploaded to S3 must be set to public-read, or users' browsers
// will get PermissionDenied errors, and torrent URLs won't work. The one exception to
// this is when all content is being routed through an edge service and access via S3
// should be blocked.
if (!empty($this->config['use_cname']) && !empty($this->config['domain']) && !empty($this->config['domain_s3_private'])) {
$options[$this->protocol]['ACL'] = 'private';
}
elseif (file_uri_scheme($this->uri) != 'private') {
// All non-private files uploaded to S3 must be set to public-read, or users' browsers
// will get PermissionDenied errors, and torrent URLs won't work.
$options[$this->protocol]['ACL'] = 'public-read';
}
// Set the Cache-Control header, if the user specified one.
if (!empty($this->config['cache_control_header'])) {
$options[$this->protocol]['CacheControl'] = $this->config['cache_control_header'];
}
if (!empty($this->config['encryption'])) {
$options[$this->protocol]['ServerSideEncryption'] = $this->config['encryption'];
}
// Legacy support: set bucket and key values that were present with
// AWS SDK v2. Used by AdvAgg module with hook_s3fs_upload_params_alter().
$options[$this->protocol]['Bucket'] = $this->config['bucket'];
$convertedPath = $this
->convertUriToKeyedPath($this->uri, FALSE);
$options[$this->protocol]['Key'] = file_uri_target($convertedPath);
// Allow other modules to alter the upload params.
drupal_alter('s3fs_upload_params', $options[$this->protocol]);
// Legacy support: unset bucket and key values. Retaining them will
// cause conflicts when saving AdvAgg's aggregated css/js files.
unset($options[$this->protocol]['Bucket']);
unset($options[$this->protocol]['Key']);
stream_context_set_option($this->context, $options);
if (parent::stream_flush()) {
$this
->writeUriToCache($this->uri);
return TRUE;
}
else {
return FALSE;
}
}
/**
* Support for unlink().
*
* @param string $uri
* The uri of the resource to delete.
*
* @return bool
* TRUE if resource was successfully deleted, regardless of whether or not
* the file actually existed.
* FALSE if the call to S3 failed, in which case the file will not be
* removed from the cache.
*
* @see http://php.net/manual/en/streamwrapper.unlink.php
*/
public function unlink($uri) {
$this
->setUri($uri);
$converted = $this
->convertUriToKeyedPath($uri);
if (parent::unlink($converted)) {
$this
->_delete_cache($uri);
clearstatcache(TRUE, $uri);
return TRUE;
}
else {
return FALSE;
}
}
public function url_stat($uri, $flags) {
$this
->setUri($uri);
return $this
->_stat($uri);
}
/**
* Support for mkdir().
*
* @param string $uri
* The URI to the directory to create.
* @param int $mode
* Permission flags - see mkdir().
* @param int $options
* A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
*
* @return bool
* TRUE if the directory was successfully created. Otherwise, FALSE.
*
* @see http://php.net/manual/en/streamwrapper.mkdir.php
*/
public function mkdir($uri, $mode, $options) {
// Some Drupal plugins call mkdir with a trailing slash. We mustn't store
// that slash in the cache.
$uri = rtrim($uri, '/');
clearstatcache(TRUE, $uri);
// If this uri already exists in the cache, return TRUE if it's a folder
// (so that recursive calls won't improperly report failure when they
// reach an existing ancestor), or FALSE if it's a file (failure).
$test_metadata = $this
->_read_cache($uri);
if ($test_metadata) {
return (bool) $test_metadata['dir'];
}
// S3 is a flat file system, with no concept of directories (just files
// with slashes in their names). We store folders in the metadata cache,
// but don't create an object for them in S3.
$metadata = _s3fs_convert_metadata($uri, array());
$this
->_write_cache($metadata);
// If the STREAM_MKDIR_RECURSIVE option was specified, also create all the
// ancestor folders of this uri, except for the root directory.
$parent_dir = drupal_dirname($uri);
if ($options & STREAM_MKDIR_RECURSIVE && file_uri_target($parent_dir) != '') {
return $this
->mkdir($parent_dir, $mode, $options);
}
return TRUE;
}
/**
* Support for rmdir().
*
* @param string $uri
* The URI to the folder to delete.
* @param int $options
* A bit mask of STREAM_REPORT_ERRORS.
*
* @return bool
* TRUE if folder is successfully removed.
* FALSE if $uri isn't a folder, or the folder is not empty.
*
* @see http://php.net/manual/en/streamwrapper.rmdir.php
*/
public function rmdir($uri, $options) {
if (!$this
->_path_is_dir($uri)) {
return FALSE;
}
// We need a version of $uri with no / because folders are cached with no /.
// We also need one with the /, because it might be a file in S3 that
// ends with /. In addition, we must differentiate against files with this
// folder's name as a substring.
// e.g. rmdir('s3://foo/bar') should ignore s3://foo/barbell.jpg.
$bare_path = rtrim($uri, '/');
$slash_path = $bare_path . '/';
// If the folder is empty, it's eligible for deletion.
$file_count = db_select('s3fs_file', 's')
->fields('s')
->condition('uri', db_like($slash_path) . '%', 'LIKE')
->execute()
->rowCount();
if ($file_count === 0) {
if (parent::rmdir($this
->convertUriToKeyedPath($uri), $options)) {
$this
->_delete_cache($uri);
clearstatcache(TRUE, $uri);
return TRUE;
}
}
// The folder is non-empty.
return FALSE;
}
/**
* Support for opendir().
*
* @param string $uri
* The URI to the directory to open.
* @param int $options
* A flag used to enable safe_mode.
* This wrapper doesn't support safe_mode, so this parameter is ignored.
*
* @return bool
* TRUE on success. Otherwise, FALSE.
*
* @see http://php.net/manual/en/streamwrapper.dir-opendir.php
*/
public function dir_opendir($uri, $options = NULL) {
if (!$this
->_path_is_dir($uri)) {
return FALSE;
}
$scheme = file_uri_scheme($uri);
$bare_path = rtrim($uri, '/');
$slash_path = $bare_path . '/';
// If this path was originally a root folder (e.g. s3://), the above code
// removed *both* slashes but only added one back. So we need to add
// back the second slash.
if ($slash_path == "{$scheme}:/") {
$slash_path = "{$scheme}://";
}
// Get the list of paths for files and folders which are children of the
// specified folder, but not grandchildren.
$child_paths = db_select('s3fs_file', 's')
->fields('s', array(
'uri',
))
->condition('uri', db_like($slash_path) . '%', 'LIKE')
->condition('uri', db_like($slash_path) . '%/%', 'NOT LIKE')
->execute()
->fetchCol(0);
$this->dir = array();
foreach ($child_paths as $child_path) {
$this->dir[] = drupal_basename($child_path);
}
return TRUE;
}
/**
* Support for readdir().
*
* @return string
* The next filename, or FALSE if there are no more files in the directory.
*
* @see http://php.net/manual/en/streamwrapper.dir-readdir.php
*/
public function dir_readdir() {
$current = current($this->dir);
if ($current) {
next($this->dir);
}
return $current;
}
public function rename($from_path, $to_path) {
// Set access for new item in stream context.
if (file_uri_scheme($from_path) != 'private') {
stream_context_set_option($this->context, 's3', 'ACL', 'public-read');
}
// If parent succeeds in renaming, updated local metadata and cache.
if (parent::rename($this
->convertUriToKeyedPath($from_path), $this
->convertUriToKeyedPath($to_path))) {
$metadata = $this
->_read_cache($from_path);
$metadata['uri'] = $to_path;
$this
->_write_cache($metadata);
$this
->_delete_cache($from_path);
clearstatcache(TRUE, $from_path);
clearstatcache(TRUE, $to_path);
return TRUE;
}
else {
return FALSE;
}
}
/***************************************************************************
Internal Functions
***************************************************************************/
/**
* Determine whether the $uri is a directory.
*
* @param string $uri
* The path of the resource to check.
*
* @return bool
* TRUE if the resource is a directory.
*/
protected function _path_is_dir($uri) {
$metadata = $this
->_s3fs_get_object($uri);
return $metadata ? $metadata['dir'] : FALSE;
}
/**
* Implementation of a stat method to ensure that remote files don't fail
* checks when they should pass.
*
* @param $uri
* @return array|bool
*/
protected function _stat($uri) {
$metadata = $this
->_s3fs_get_object($uri);
if ($metadata) {
$stat = array();
$stat[0] = $stat['dev'] = 0;
$stat[1] = $stat['ino'] = 0;
// Use the S_IFDIR posix flag for directories, S_IFREG for files.
// All files are considered writable, so OR in 0777.
$stat[2] = $stat['mode'] = ($metadata['dir'] ? 040000 : 0100000) | 0777;
$stat[3] = $stat['nlink'] = 0;
$stat[4] = $stat['uid'] = 0;
$stat[5] = $stat['gid'] = 0;
$stat[6] = $stat['rdev'] = 0;
$stat[7] = $stat['size'] = 0;
$stat[8] = $stat['atime'] = 0;
$stat[9] = $stat['mtime'] = 0;
$stat[10] = $stat['ctime'] = 0;
$stat[11] = $stat['blksize'] = 0;
$stat[12] = $stat['blocks'] = 0;
if (!$metadata['dir']) {
$stat[4] = $stat['uid'] = 's3fs';
$stat[7] = $stat['size'] = $metadata['filesize'];
$stat[8] = $stat['atime'] = $metadata['timestamp'];
$stat[9] = $stat['mtime'] = $metadata['timestamp'];
$stat[10] = $stat['ctime'] = $metadata['timestamp'];
}
return $stat;
}
return FALSE;
}
/**
* Try to fetch an object from the metadata cache.
*
* If that file isn't in the cache, we assume it does not exist.
*
* @param string $uri
* The uri of the resource to check.
*
* @return array|bool
* An array if the $uri exists, otherwise FALSE.
*/
protected function _s3fs_get_object($uri) {
// For the root directory, return metadata for a generic folder.
if (file_uri_target($uri) == '') {
return _s3fs_convert_metadata('/', array());
}
// Trim any trailing '/', in case this is a folder request.
$uri = rtrim($uri, '/');
// Check if this uri is in the cache.
$metadata = $this
->_read_cache($uri);
// If cache ignore is enabled, query S3 for all uris which aren't in the
// cache, and non-folder uris which are.
if (!empty($this->config['ignore_cache']) && !$metadata['dir']) {
try {
// If _get_metadata_from_s3() returns FALSE, the file doesn't exist.
$metadata = $this
->_get_metadata_from_s3($uri);
} catch (\Exception $e) {
return $this
->_trigger_error($e
->getMessage());
}
}
return $metadata;
}
/**
* Fetch an object from the file metadata cache table.
*
* @param string $uri
* The uri of the resource to check.
*
* @return array|bool
* An array of metadata if the $uri is in the cache. Otherwise, FALSE.
*/
protected function _read_cache($uri) {
// Since public:///blah.jpg and public://blah.jpg refer to the same file
// (a file named blah.jpg at the root of the file system), we'll sometimes
// receive files with a /// in their uri. This messes with our caching
// scheme, though, so we need to remove the extra /.
if (strpos($uri, 'public:///') === 0) {
$uri = preg_replace('^public://[/]+^', 'public://', $uri);
}
elseif (strpos($uri, 'private:///') === 0) {
$uri = preg_replace('^private://[/]+^', 'private://', $uri);
}
$record = db_select('s3fs_file', 's')
->fields('s')
->condition('uri', $uri, '=')
->execute()
->fetchAssoc();
return $record ? $record : FALSE;
}
/**
* Write an object's (and its ancestor folders') metadata to the cache.
*
* @param array $metadata
* An associative array of file metadata in this format:
* 'uri' => The full URI of the file, including the scheme.
* 'filesize' => The size of the file, in bytes.
* 'timestamp' => The file's create/update timestamp.
* 'dir' => A boolean indicating whether the object is a directory.
*
* @throws
* Exceptions which occur in the database call will percolate.
*/
protected function _write_cache($metadata) {
// Since public:///blah.jpg and public://blah.jpg refer to the same file
// (a file named blah.jpg at the root of the file system), we'll sometimes
// receive files with a /// in their URI. This messes with our caching
// scheme, though, so we need to remove the extra /.
if (strpos($metadata['uri'], 'public:///') === 0) {
$metadata['uri'] = preg_replace('^public://[/]+^', 'public://', $metadata['uri']);
}
elseif (strpos($metadata['uri'], 'private:///') === 0) {
$metadata['uri'] = preg_replace('^private://[/]+^', 'private://', $metadata['uri']);
}
db_merge('s3fs_file')
->key(array(
'uri' => $metadata['uri'],
))
->fields($metadata)
->execute();
$dirname = drupal_dirname($metadata['uri']);
// If this file isn't in the root directory, also write this file's
// ancestor folders to the cache.
if (file_uri_target($dirname) != '') {
$this
->mkdir($dirname, NULL, STREAM_MKDIR_RECURSIVE);
}
}
/**
* Delete an object's metadata from the cache.
*
* @param mixed $uri
* A string (or array of strings) containing the uri(s) of the object(s)
* to be deleted.
*
* @return object|bool
* Returns a query object or FALSE.
*
* @throws
* Exceptions which occur in the database call will percolate.
*/
protected function _delete_cache($uri) {
if (!is_array($uri)) {
$uri = array(
$uri,
);
}
// Build an OR query to delete all the URIs at once.
$delete_query = db_delete('s3fs_file');
$or = db_or();
foreach ($uri as $u) {
$or
->condition('uri', $u, '=');
}
$delete_query
->condition($or);
return $delete_query
->execute();
}
/**
* Get the stream's context options or remove them if wanting default.
*
* @param bool $removeContextData
* Whether to remove the stream's context information.
*
* @return array
* An array of options.
*/
public function getOptions($removeContextData = false) {
// Context is not set when doing things like stat
if (is_null($this->context)) {
$this->context = stream_context_get_default();
}
$options = stream_context_get_options($this->context);
if ($removeContextData) {
unset($options['client'], $options['seekable'], $options['cache']);
}
return $options;
}
/**
* Converts a Drupal URI path into what is expected to be stored in S3.
*
* @param $uri
* An appropriate URI formatted like 'protocol://path'.
* @param bool $prepend_bucket
* Whether to prepend the bucket name. S3's stream wrapper requires this for
* some functions.
*
* @return string
* A converted string ready for S3 to process it.
*/
protected function convertUriToKeyedPath($uri, $prepend_bucket = TRUE) {
// Remove the protocol
$parts = explode('://', $uri);
if (!empty($parts[1])) {
// public:// file are all placed in the s3fs_public_folder.
$public_folder = !empty($this->config['public_folder']) ? $this->config['public_folder'] : 's3fs-public';
$private_folder = !empty($this->config['private_folder']) ? $this->config['private_folder'] : 's3fs-private';
if (file_uri_scheme($uri) == 'public') {
$parts[1] = "{$public_folder}/{$parts[1]}";
}
elseif (file_uri_scheme($uri) == 'private') {
$parts[1] = "{$private_folder}/{$parts[1]}";
}
// If it's set, all files are placed in the root folder.
if (!empty($this->config['root_folder'])) {
$parts[1] = "{$this->config['root_folder']}/{$parts[1]}";
}
// Prepend the uri with a bucket since AWS SDK expects this.
if ($prepend_bucket) {
$parts[1] = $this->config['bucket'] . '/' . $parts[1];
}
}
// Set protocol to S3 so AWS stream wrapper works correctly.
$parts[0] = 's3';
return implode('://', $parts);
}
/**
* Return bucket and key for a command array.
*
* @param string $uri
* Uri to the required object.
*
* @return array
* A modified path to the key in S3.
*/
protected function getCommandParams($uri) {
$convertedPath = $this
->convertUriToKeyedPath($uri, FALSE);
$params = $this
->getOptions(true);
$params['Bucket'] = $this->config['bucket'];
$params['Key'] = file_uri_target($convertedPath);
return $params;
}
/**
* Returns the converted metadata for an object in S3.
*
* @param string $uri
* The uri for the object in S3.
*
* @return array|bool
* An array of DB-compatible file metadata.
*
* @throws \Exception
* Any exception raised by the listObjects() S3 command will percolate
* out of this function.
*/
protected function _get_metadata_from_s3($uri) {
$params = $this
->getCommandParams($uri);
try {
$result = $this->s3
->headObject($params);
} catch (S3Exception $e) {
// headObject() throws this exception if the requested key doesn't exist
// in the bucket.
watchdog_exception('S3FS', $e);
return FALSE;
}
return _s3fs_convert_metadata($uri, $result);
}
/**
* Triggers one or more errors.
*
* @param string|array $errors
* Errors to trigger.
* @param mixed $flags
* If set to STREAM_URL_STAT_QUIET, no error or exception is triggered.
*
* @return bool
* Always returns FALSE.
*
* @throws RuntimeException
* If the 'throw_exceptions' option is TRUE.
*/
protected function _trigger_error($errors, $flags = NULL) {
if ($flags != STREAM_URL_STAT_QUIET) {
trigger_error(implode("\n", (array) $errors), E_USER_ERROR);
}
$this->_error_state = TRUE;
return FALSE;
}
/**
* Helper function to safely append a GET argument to a given base URL.
*
* @param string $base_url
* The URL onto which the GET arg will be appended.
* @param string $name
* The name of the GET argument.
* @param string $value
* The value of the GET argument. Optional.
*
* @return string
* The converted path GET argument.
*/
protected static function _append_get_arg($base_url, $name, $value = NULL) {
$separator = strpos($base_url, '?') === FALSE ? '?' : '&';
$new_url = "{$base_url}{$separator}{$name}";
if ($value !== NULL) {
$new_url .= "={$value}";
}
return $new_url;
}
}
Members
Name![]() |
Modifiers | Type | Description | Overrides |
---|---|---|---|---|
S3fsStreamWrapper:: |
private | property | Module configuration for stream. | |
S3fsStreamWrapper:: |
public | property | Stream context (this is set by PHP when a context is used). | |
S3fsStreamWrapper:: |
protected | property | Directory listing used by the dir_* methods. | |
S3fsStreamWrapper:: |
protected | property | Domain we use to access files over http. | |
S3fsStreamWrapper:: |
protected static | property | Default map for determining file mime types. | |
S3fsStreamWrapper:: |
private | property | Mode in which the stream was opened. | |
S3fsStreamWrapper:: |
private | property | Hash of opened stream parameters. | |
S3fsStreamWrapper:: |
protected | property | Files which should be created with URLs that eventually time out. | |
S3fsStreamWrapper:: |
private | property | The opened protocol (e.g., "s3"). | |
S3fsStreamWrapper:: |
protected | property | The AWS SDK for PHP S3Client object. | |
S3fsStreamWrapper:: |
protected | property | Files that the user has said must be downloaded, rather than viewed. | |
S3fsStreamWrapper:: |
protected | property | Map for files that should be delivered with a torrent URL. | |
S3fsStreamWrapper:: |
protected | property | Instance URI referenced as "<scheme>://key". | |
S3fsStreamWrapper:: |
protected | property | Indicates the current error state in the wrapper. | |
S3fsStreamWrapper:: |
public | function |
This wrapper doesn't support file permissions. Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
protected | function | Converts a Drupal URI path into what is expected to be stored in S3. | |
S3fsStreamWrapper:: |
public | function |
Gets the name of the parent directory of a given path. Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for opendir(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for readdir(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
protected | function | Return bucket and key for a command array. | |
S3fsStreamWrapper:: |
public | function | Gets the path that the wrapper is responsible for. | |
S3fsStreamWrapper:: |
public | function |
Returns a web accessible URL for the resource. Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public static | function |
Returns the MIME type of the resource. Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function | Get the stream's context options or remove them if wanting default. | |
S3fsStreamWrapper:: |
public | function |
Returns the stream resource URI, which looks like "<scheme>://filepath". Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for mkdir(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
This wrapper does not support realpath(). Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public static | function | Ensure the S3 protocol is registered to this class and not parents. | |
S3fsStreamWrapper:: |
public | function |
Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for rmdir(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Sets the stream resource URI. URIs are formatted as "<scheme>://filepath". Overrides DrupalStreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for fflush(). Flush current cached stream data to a file in S3. Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
This wrapper does not support flock(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function | ||
S3fsStreamWrapper:: |
public | function |
Support for fopen(), file_get_contents(), file_put_contents() etc. Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Support for unlink(). Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function |
Overrides StreamWrapperInterface:: |
|
S3fsStreamWrapper:: |
public | function | Wait for the specified file to exist in the bucket. | |
S3fsStreamWrapper:: |
public | function | Write the file at the given uri into the metadata cache. | |
S3fsStreamWrapper:: |
protected static | function | Helper function to safely append a GET argument to a given base URL. | |
S3fsStreamWrapper:: |
protected | function | Delete an object's metadata from the cache. | |
S3fsStreamWrapper:: |
protected | function | Returns the converted metadata for an object in S3. | |
S3fsStreamWrapper:: |
protected | function | Determine whether the $uri is a directory. | |
S3fsStreamWrapper:: |
protected | function | Fetch an object from the file metadata cache table. | |
S3fsStreamWrapper:: |
protected | function | Try to fetch an object from the metadata cache. | |
S3fsStreamWrapper:: |
protected | function | Implementation of a stat method to ensure that remote files don't fail checks when they should pass. | |
S3fsStreamWrapper:: |
protected | function | Triggers one or more errors. | |
S3fsStreamWrapper:: |
protected | function | Write an object's (and its ancestor folders') metadata to the cache. | |
S3fsStreamWrapper:: |
public | function | Stream wrapper constructor. | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 | |
StreamWrapperInterface:: |
public | function | 1 |