amazons3.module in AmazonS3 7.2
Same filename and directory in other branches
Hook implementations for the AmazonS3 module.
File
amazons3.moduleView source
<?php
use Aws\Common\Credentials\Credentials;
use Drupal\amazons3\Exception\S3ConnectValidationException;
use Drupal\amazons3\S3Client;
use Drupal\amazons3\S3Url;
use Drupal\amazons3\StreamWrapperConfiguration;
/**
* @file
* Hook implementations for the AmazonS3 module.
*/
/**
* Implements hook_stream_wrappers().
*
* Create a stream wrapper for S3.
*/
function amazons3_stream_wrappers() {
// This hook is called before hook_init(), so we have to manually register
// the autoloader. We also need to handle module upgrades where
// composer_manager might not be enabled yet.
if (!module_exists('composer_manager')) {
return array();
}
// If the module has been enabled, but the user didn't update composer
// libraries, prevent failing entirely.
try {
composer_manager_register_autoloader();
} catch (\RuntimeException $e) {
watchdog('amazons3', 'The Composer autoloader could not be registered. Run drush composer-rebuild and drush composer-manager update to update your vendor directory.');
watchdog_exception('amazons3', $e);
return array();
}
if (!class_exists('Drupal\\amazons3\\StreamWrapper')) {
watchdog('amazons3', 'The AmazonS3 StreamWrapper class is missing. Make sure all module updates have run. Otherwise, run drush composer-rebuild and drush composer-manager update to update your vendor directory.');
return array();
}
// If the module isn't configured yet, don't register the stream wrapper.
try {
StreamWrapperConfiguration::fromDrupalVariables();
} catch (\InvalidArgumentException $e) {
if (current_path() == 'admin/config/media/file-system') {
drupal_set_message(t('The AmazonS3 module <a href="@s3-configure">needs to be configured</a> before setting it as a download method.', array(
'@s3-configure' => url('admin/config/media/amazons3', array(
'query' => array(
'destination' => current_path(),
),
)),
)));
}
return array();
}
// getimagesize() calls require the stream to be seekable.
stream_context_set_default([
's3' => [
'seekable' => TRUE,
],
]);
return array(
's3' => array(
'name' => 'Amazon S3',
'class' => 'Drupal\\amazons3\\StreamWrapper',
'description' => t('Amazon Simple Storage Service'),
),
);
}
/**
* Implements hook_menu().
*/
function amazons3_menu() {
$items = array();
$items['admin/config/media/amazons3'] = array(
'title' => 'Amazon S3',
'description' => 'Configure S3 credentials and settings',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'amazons3_admin',
),
'access arguments' => array(
'administer amazons3',
),
'file' => 'amazons3.admin.inc',
);
// hook_menu is called after this module is enabled, but before Composer
// dependencies are enabled. This menu callback string should always match
// \Drupal\amazons3\StreamWrapper::stylesCallback.
$items['amazons3/image-derivative'] = array(
'title' => 'Image style delivery callback',
'description' => 'Callback to generate an image derivative, upload it to S3, and redirect to the S3 URL',
'page callback' => 'amazons3_image_deliver',
'access arguments' => array(
'access content',
),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Image delivery callback that uploads a derivative to S3.
*
* @param ...
* The path components of the source image.
*/
function amazons3_image_deliver() {
$args = func_get_args();
if (count($args) < 4) {
return MENU_NOT_FOUND;
}
$bucket = $args[0];
// Pop off the bucket and the 'styles' constant in the URL.
array_shift($args);
array_shift($args);
$style_name = $args[0];
// If the image style doesn't exist, we can return early.
if (!($style = image_style_load($style_name))) {
return MENU_NOT_FOUND;
}
// Pop off the style name; the rest is our key to the original image.
array_shift($args);
$path = $args;
$key = implode('/', $path);
$source = new S3Url($bucket, $key);
$destination_s3 = $source
->getImageStyleUrl($style_name);
// Check that the image style token is valid.
if (!variable_get('image_allow_insecure_derivatives', FALSE) || strpos($destination_s3
->getKey(), 'styles/') === 0) {
$valid = isset($_GET[IMAGE_DERIVATIVE_TOKEN]) && $_GET[IMAGE_DERIVATIVE_TOKEN] === image_style_path_token($style['name'], (string) $source);
if (!$valid) {
return MENU_ACCESS_DENIED;
}
}
if (!file_exists($destination_s3)) {
// If there is no source image we can 404 early.
if (!file_exists($source)) {
return MENU_NOT_FOUND;
}
$lock_name = 'amazons3_image_style_deliver:' . drupal_hash_base64($destination_s3
->getKey());
$destination_temp = 'temporary://amazons3/' . $destination_s3
->getKey();
// Prevent cache stampedes.
if (!lock_acquire($lock_name)) {
_amazons3_image_wait_transfer($destination_temp);
}
// If the temporary file exists, we assume another thread has generated it
// and we can transfer it directly.
if (!file_exists($destination_temp)) {
$image = _amazons3_generate_image_style($source, $style, $destination_temp, $destination_s3);
}
else {
$image = amazons3_image_load($destination_temp);
}
lock_release($lock_name);
// Transfer the image to the client from our temporary directory.
file_transfer($image->source, array(
'Content-Type' => $image->info['mime_type'],
'Content-Length' => $image->info['file_size'],
));
}
// If the file exists on S3, send a permanent redirect.
/** @var \Drupal\amazons3\StreamWrapper $wrapper */
$wrapper = file_stream_wrapper_get_instance_by_uri($destination_s3);
drupal_goto($wrapper
->getExternalUrl(), array(), 301);
}
/**
* Generate an image style derivative and upload it to S3.
*
* @param S3Url $source
* The URL of the source image stored in S3.
* @param string $style
* The name of the image style to generate the derivative for.
* @param string $destination_temp
* The temporary:// path to save the generated image in.
* @param S3Url $destination_s3
*
* @throws \Exception
* Thrown if image_style_create_derivative() failed.
*
* @return \stdClass
* The generated image object.
*
*/
function _amazons3_generate_image_style(S3Url $source, $style, $destination_temp, S3Url $destination_s3) {
// Before we create the file, we need to see if a row exists in
// {files_managed}. This can happen when a temporary directory is cleared
// by a server reboot or manually by a system administrator.
$q = new EntityFieldQuery();
$q
->entityCondition('entity_type', 'file')
->propertyCondition('uri', $destination_temp);
$results = $q
->execute();
if (!empty($results['file'])) {
$file = reset($results['file']);
$file = file_load($file->fid);
file_delete($file);
}
// Generate the derivative.
if (!image_style_create_derivative($style, $source, $destination_temp)) {
// Something went horribly wrong, but all we have is a FALSE return. Throw
// an exception with something useful.
throw new \Exception('Amazon S3 was unable to create an image style derivative. Check the temporary directory configuration and permissions.');
}
// We need to manage our temporary file so it is cleaned by system_cron().
$file = amazons3_file_create_object($destination_temp);
file_save($file);
// Register a shutdown function to upload the image to S3.
$image = amazons3_image_load($destination_temp);
register_shutdown_function(function () use ($image, $destination_s3) {
// We have to call both of these to actually flush the image.
ob_end_flush();
flush();
// file_unmanaged_copy() will not create any nested directories if
// needed.
$directory = drupal_dirname($destination_s3);
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
watchdog('amazons3', 'Failed to create style directory: %directory', array(
'%directory' => $directory,
), WATCHDOG_ERROR);
}
file_unmanaged_copy($image->source, $destination_s3);
});
return $image;
}
/**
* Create a file object.
*
* @param string $uri
* The URI of the object to create.
*
* @return \stdClass
* A file object suitable to use with file_save().
*/
function amazons3_file_create_object($uri) {
$file = new stdClass();
$file->fid = NULL;
$file->uri = $uri;
$file->filename = drupal_basename($uri);
$file->filemime = file_get_mimetype($file->uri);
$file->uid = 0;
$file->status = 0;
return $file;
}
/**
* Wait for an image to appear in a directory, and transfer it when it appears.
*
* @param string $uri
* The image URI to transfer.
*
* @return bool
* FALSE if the image was not transferred.
*/
function _amazons3_image_wait_transfer($uri) {
// Another process is trying to create the file on S3. S3 uploads can
// still be slow, so we wait for the temporary file to exist and serve
// that.
$tries = 0;
while ($tries < 4 && !file_exists($uri)) {
usleep(500000);
$tries++;
}
// If the file doesn't exist, it either means we had a stale lock, or the
// other process died and couldn't create the image style. In that case,
// we fall through and try to create the derivative without acquiring the
// lock.
if (file_exists($uri)) {
$image = amazons3_image_load($uri);
file_transfer($uri, array(
'Content-Type' => $image->info['mime_type'],
'Content-Length' => $image->info['file_size'],
));
}
return FALSE;
}
/**
* Load an image, exiting if it could not be loaded.
*
* @param string $uri
* The image URI to load.
*
* @return \stdClass
* The loaded image.
*/
function amazons3_image_load($uri) {
$image = image_load($uri);
if (!$image) {
watchdog('amazons3', 'Unable to generate the derived image located at %path.', array(
'%path' => $uri,
));
drupal_add_http_header('Status', '500 Internal Server Error');
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
print t('Error generating image.');
drupal_exit();
}
return $image;
}
/**
* Implements hook_permission().
*/
function amazons3_permission() {
return array(
'administer amazons3' => array(
'title' => t('Administer AmazonS3'),
),
);
}
/**
* Implements hook_flush_caches().
*/
function amazons3_flush_caches() {
return array(
'cache_amazons3_metadata',
);
}
/**
* Implements hook_field_info_alter().
*/
function amazons3_field_info_alter(&$info) {
foreach (amazons3_file_like_field() as $type) {
// Use the default bucket as specified in the module configuration.
if (isset($info[$type])) {
$info[$type]['settings']['amazons3_bucket'] = variable_get('amazons3_bucket', '');
}
}
}
/**
* Implements hook_image_style_path_alter().
*
* When we are using S3, we need to rewrite image style URLs to route through
* our own paths.
*/
function amazons3_image_style_path_alter(&$result, $style_name, $uri) {
$scheme = file_uri_scheme($uri);
if ($scheme != 's3') {
return;
}
$s3url = S3Url::factory($uri);
$result = $s3url
->getImageStyleUrl($style_name);
}
/**
* Return an array of field types that are like a file field.
*
* If a field type is calling file_* hooks to create it's field, it likely
* belongs here.
*
* @return array
* An array of field types.
*/
function amazons3_file_like_field() {
return array(
'file',
'image',
);
}
/**
* Implements hook_field_widget_form_alter().
*
* Override file fields to use our destination function to determine the
* upload location for a file.
*/
function amazons3_field_widget_form_alter(&$element, &$form_state, $context) {
$field = $context['field'];
$instance = $context['instance'];
if (in_array($field['type'], amazons3_file_like_field())) {
if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
$delta = $context['delta'];
$element[$delta]['#upload_location'] = amazons3_field_widget_uri($field, $instance);
}
else {
// In this case this alter hook is only called once for all items in the
// widget.
foreach (element_children($element) as $delta) {
$element[$delta]['#upload_location'] = amazons3_field_widget_uri($field, $instance);
}
}
}
}
/**
* Return the destination URI for a file field.
*
* @param array $field
* A field array.
* @param array $instance
* A field instance array.
* @param array $data
* (optional) An array of token objects to pass to token_replace().
*
* @see file_field_widget_uri()
* @see token_replace()
*
* @return string
* A file directory URI with tokens replaced.
*/
function amazons3_field_widget_uri(array $field, array $instance, array $data = array()) {
$uri_scheme = $field['settings']['uri_scheme'];
$file_directory = isset($instance['settings']['file_directory']) ? $instance['settings']['file_directory'] : NULL;
$bucket = isset($field['settings']['amazons3_bucket']) ? $field['settings']['amazons3_bucket'] : NULL;
return amazons3_upload_location($uri_scheme, $bucket, $file_directory, $data);
}
/**
* Return a URI for use in an #upload_location or similar form element.
*
* @param string $uri_scheme
* The scheme to use for the URI.
* @param string $bucket
* (optional) bucket, if the URI is an s3 URI.
* @param string $file_directory
* (optional) File directory for the URI.
* @param array $data
* (optional) Array of data to use when replacing tokens.
*
* @return string
* A fully-qualified string URI.
*/
function amazons3_upload_location($uri_scheme, $bucket = NULL, $file_directory = NULL, array $data = array()) {
if ($uri_scheme == 's3') {
$destination = $bucket;
// If no bucket is specified, but this is an S3 URI, use the default bucket.
if (empty($destination)) {
$config = StreamWrapperConfiguration::fromDrupalVariables();
$destination = $config
->getBucket();
}
if (!empty($file_directory)) {
$destination .= '/' . trim($file_directory, '/');
}
}
else {
$destination = trim($file_directory, '/');
}
// Replace tokens.
$destination = token_replace($destination, $data);
return $uri_scheme . '://' . $destination;
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function amazons3_form_field_ui_field_settings_form_alter(&$form, &$form_state, $form_id) {
$type = $form['field']['type']['#value'];
_amazons3_field_configuration($form, $type);
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Add bucket configuration to each file field form.
*/
function amazons3_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
$type = $form['#field']['type'];
_amazons3_field_configuration($form, $type);
}
/**
* Implements hook_file_entity_upload_destination_uri_alter().
*/
function amazons3_file_entity_upload_destination_uri_alter(&$result, array $params = array(), array $data = array()) {
if ($params['uri_scheme'] == 's3') {
try {
$url = S3Url::factory($result);
$bucket = $url
->getBucket();
$s3 = S3Client::factory(array(), $bucket);
S3Client::validateBucketExists($bucket, $s3, new \Drupal\amazons3\Cache());
} catch (\InvalidArgumentException $e) {
// We couldn't parse the URL, so check to see if it is bare.
if ($result == 's3://') {
$config = StreamWrapperConfiguration::fromDrupalVariables();
$bucket = $config
->getBucket();
}
else {
throw $e;
}
} catch (S3ConnectValidationException $e) {
if (!empty($params['field']) && ($field = field_info_field($params['field']))) {
$bucket = $field['settings']['amazons3_bucket'];
}
else {
$config = StreamWrapperConfiguration::fromDrupalVariables();
$bucket = $config
->getBucket();
}
}
$result = amazons3_upload_location('s3', $bucket, $params['file_directory'], $data);
}
}
/**
* Implements hook_file_stream_wrapper_uri_normalize_alter().
*/
function amazons3_file_stream_wrapper_uri_normalize_alter(&$uri, $scheme, $target) {
if ($scheme == 's3') {
try {
// If this try passes, $uri is a fully-formed s3:// URI with a bucket.
$url = S3Url::factory($uri);
// Validate that the bucket exists. Sometimes we might be passed in URIs
// without a bucket, like s3://image.jpg. If image.jpg is not a bucket, we
// assume that image.jpg is supposed to be created in the default bucket.
$bucket = $url
->getBucket();
$s3 = S3Client::factory(array(), $bucket);
S3Client::validateBucketExists($bucket, $s3, new \Drupal\amazons3\Cache());
} catch (\InvalidArgumentException $e) {
// Catch if S3Url::factory() can not parse $uri. That happens if we are
// passed in a bare URI like s3://. Fall back to the default bucket.
$uri = amazons3_uri_add_bucket($target);
} catch (S3ConnectValidationException $e) {
// Catch if a bucket does not exist or is invalid.
$uri = amazons3_uri_add_bucket($target);
}
}
}
/**
* Add the default bucket and return a string URL.
*
* @param string $target
* The file path to return the URL for.
*
* @return string
* A fully-qualified s3:// URL.
*/
function amazons3_uri_add_bucket($target) {
$config = StreamWrapperConfiguration::fromDrupalVariables();
$url = new S3Url($config
->getBucket(), $target);
return (string) $url;
}
/**
* Add S3 configuration to file field settings forms.
*
* @param array &$form
* The form to alter.
* @param string $type
* The field type being modified.
*/
function _amazons3_field_configuration(array &$form, $type) {
foreach (amazons3_file_like_field() as $types) {
if ($type == $types) {
$settings =& $form['field']['settings'];
$bucket_setting = isset($form['#field']['settings']['amazons3_bucket']) ? $form['#field']['settings']['amazons3_bucket'] : '';
$region_setting = isset($form['#field']['settings']['amazons3_region']) ? $form['#field']['settings']['amazons3_region'] : '';
$settings['uri_scheme']['#weight'] = 50;
$settings['amazons3_bucket'] = array(
'#type' => 'textfield',
'#title' => t('Amazon S3 bucket'),
'#description' => t('Leave blank to use the site-wide default bucket <a href="@config">currently set to %bucket</a>.', array(
'@config' => url('admin/config/media/amazons3'),
'%bucket' => variable_get('amazons3_bucket', ''),
)),
'#states' => array(
'visible' => array(
':input[name="field[settings][uri_scheme]"]' => array(
'value' => 's3',
),
),
),
'#default_value' => $bucket_setting,
'#element_validate' => array(
'amazons3_form_bucket_validate',
),
'#weight' => 51,
);
$client = \Aws\S3\S3Client::factory();
$settings['amazons3_region'] = array(
'#type' => 'select',
'#title' => t('Amazon S3 region'),
'#description' => t('Use <em>default region</em> to use the site-wide default region <a href="@config">currently set to %region</a>.', array(
'@config' => url('admin/config/media/amazons3'),
'%region' => variable_get('amazons3_region', ''),
)),
'#states' => array(
'visible' => array(
':input[name="field[settings][uri_scheme]"]' => array(
'value' => 's3',
),
),
),
'#default_value' => $region_setting,
'#options' => array(
0 => t('- default region -'),
) + array_combine(array_keys($client
->getRegions()), array_keys($client
->getRegions())),
'#weight' => 51,
);
}
}
}
/**
* Element validate callback to validate a bucket name.
*
* @param array &$element
* The element to validate.
* @param array &$form_state
* The current state of the form.
* @param array $form
* The current form.
*/
function amazons3_form_bucket_validate(array &$element, array &$form_state, array $form) {
$bucket = $element['#value'];
if (empty($bucket)) {
return;
}
if (!isset($form_state['values']['amazons3_hostname'])) {
$hostname = variable_get('amazons3_hostname');
}
else {
$hostname = $form_state['values']['amazons3_hostname'];
}
// Inject our credentials for testing.
$config = array();
if (isset($form_state['values']['amazons3_key']) && isset($form_state['values']['amazons3_secret'])) {
$config['credentials'] = new Credentials($form_state['values']['amazons3_key'], $form_state['values']['amazons3_secret']);
}
if (isset($form_state['values']['amazons3_region'])) {
$config['region'] = $form_state['values']['amazons3_region'];
}
elseif (isset($form_state['values']['field']['settings']['amazons3_region']) && $form_state['values']['field']['settings']['amazons3_region']) {
$config['region'] = $form_state['values']['field']['settings']['amazons3_region'];
}
if (!empty($hostname)) {
$config['endpoint'] = $hostname;
}
try {
$s3 = S3Client::factory($config, $bucket);
S3Client::validateBucketExists($bucket, $s3);
} catch (S3ConnectValidationException $e) {
form_error($element, t('The S3 access credentials are invalid or the bucket does not exist.'));
watchdog_exception('amazons3', $e);
} catch (Exception $e) {
form_error($element, t('There was a problem connecting to S3. The following exception was thrown: @exception', array(
'@exception' => $e
->getMessage(),
)));
watchdog_exception('amazons3', $e);
}
}
/**
* Implements hook_field_default_field_bases_alter().
*
* Allows a variable to override all exported field bases to use 'Amazon S3' as
* the Upload destination. For example this can be added to environment-specific
* Drupal settings files, to allow certain environments to upload to S3 while
* other environments upload to the exported (public or private) URI scheme:
* @code
* $conf['amazons3_file_uri_scheme_override'] = 's3';
* @endcode
*/
function amazons3_field_default_field_bases_alter(&$fields) {
if ($uri_scheme = variable_get('amazons3_file_uri_scheme_override', FALSE)) {
foreach ($fields as $key => $item) {
if (isset($item['settings']['uri_scheme'])) {
$fields[$key]['settings']['uri_scheme'] = $uri_scheme;
}
}
}
}
Functions
Name | Description |
---|---|
amazons3_field_default_field_bases_alter | Implements hook_field_default_field_bases_alter(). |
amazons3_field_info_alter | Implements hook_field_info_alter(). |
amazons3_field_widget_form_alter | Implements hook_field_widget_form_alter(). |
amazons3_field_widget_uri | Return the destination URI for a file field. |
amazons3_file_create_object | Create a file object. |
amazons3_file_entity_upload_destination_uri_alter | Implements hook_file_entity_upload_destination_uri_alter(). |
amazons3_file_like_field | Return an array of field types that are like a file field. |
amazons3_file_stream_wrapper_uri_normalize_alter | Implements hook_file_stream_wrapper_uri_normalize_alter(). |
amazons3_flush_caches | Implements hook_flush_caches(). |
amazons3_form_bucket_validate | Element validate callback to validate a bucket name. |
amazons3_form_field_ui_field_edit_form_alter | Implements hook_form_FORM_ID_alter(). |
amazons3_form_field_ui_field_settings_form_alter | Implements hook_form_FORM_ID_alter(). |
amazons3_image_deliver | Image delivery callback that uploads a derivative to S3. |
amazons3_image_load | Load an image, exiting if it could not be loaded. |
amazons3_image_style_path_alter | Implements hook_image_style_path_alter(). |
amazons3_menu | Implements hook_menu(). |
amazons3_permission | Implements hook_permission(). |
amazons3_stream_wrappers | Implements hook_stream_wrappers(). |
amazons3_upload_location | Return a URI for use in an #upload_location or similar form element. |
amazons3_uri_add_bucket | Add the default bucket and return a string URL. |
_amazons3_field_configuration | Add S3 configuration to file field settings forms. |
_amazons3_generate_image_style | Generate an image style derivative and upload it to S3. |
_amazons3_image_wait_transfer | Wait for an image to appear in a directory, and transfer it when it appears. |