Hero

Como crear un stream personalizado similar a private:// en Drupal 7

Febrero 28, 2013

enzo

Es casi seguro que todos los usuario de Drupal hayan usado los streams wrapper ../assets/ y private:// los cuales vienes incluidos en el core de Drupal 7.

Estos streams wrapper tiene dos funciones primordiales

  1. Ocultar la URL real de los archivos
  2. Manejar un nivel extra de permisos en la caso del stream wrapper private, el cual valida todas la implementaciones del hook_file_download antes de entregar el archivo solicitado.
  3. Mantener los archivos que pertenecientes a cada stream wrapper en folder separados o hasta en sistemas de archivos diferentes.

A continuación veremos como crear un custom stream wrapper llamado cloud.

  1. Registrar el stream wrapper personalizado.

Mediante la implementación de hook hook_stream_wrappers le informaremos a Drupal de la existencia de un nuevo stream wrapper llamado cloud, como se puede apreciar en el siguiente listado

/**
 * Implements hook_stream_wrappers().
 */
function MIMODULO_stream_wrappers() {
 $wrappers = array();

 // Only register the private file stream wrapper if a file path has been set.
 if (variable_get('private_cloud_stream_path', FALSE)) {
 $wrappers['cloud'] = array(
 'name' => t('Private files in cloud'),
 'class' => 'CloudStreamWrapper',
 'description' => t('Private cloud files served by Drupal.'),
 'type' => STREAM_WRAPPERS_WRITE_VISIBLE,
 'dynamic' => TRUE,
 );
 }

 return $wrappers;
}
  1. Implementar clase CloudStreamWrapper.

Se implementara la clase CloudStreamWrapper la cual se encargara de interactuar con el sistema para proveer la ruta privada y la ruta real de los archivos como se muestra en el siguiente listado.

<?php

/**
 * Cloud (cloud://) stream wrapper class.
 *
 * Provides support for storing privately accessible dyncamic files with the
 * Drupal file interface.
 *
 * Extends DrupalLocalStreamWrapper.
 */
class CloudStreamWrapper extends DrupalLocalStreamWrapper {
 /**
 * Implements abstract public function getDirectoryPath()
 */
 public function getDirectoryPath() {
 return variable_get('private_cloud_stream_path', '');
 }

 /**
 * Overrides getExternalUrl().
 *
 * Return the HTML URI of a private dynamic file.
 */
 function getExternalUrl() {
 $path = str_replace('\\', '/', $this->getTarget());
 return url('system/cloud/' . $path, array('absolute' => TRUE));
 }
 
}

Esta clase se debe estar en un archivo dentro de nuestro módulo que puede ser llamado CloudStreamWrapper.inc y para garantizarnos que este disponible, lo registraremos dentro del archivo .info de nuestro módulo como se muestra a continuación.

files[] = CloudStreamWrapper.inc
  1. Definir path para el stream wrapper cloud.

Como se observa en el código anterior el stream wrapper solo es registrado en Drupal si la ruta para el stream wrapper se ha definido, por lo tanto se agregara una interfaz donde se pueda definir. Esta interfaz estará presente en el mismo formulario donde se definen las rutas para los stream wrappers public y private como se muestra en el siguiente listado de código.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function MIMODULO_form_system_file_system_settings_alter(&$form, $form_state, $form_id) {

 $form['private_cloud_stream_path'] = array(
 '#type' => 'textfield',
 '#title' => t('Private cloud file system path'),
 '#default_value' => variable_get('private_cloud_stream_path', ''),
 '#maxlength' => 255,
 '#description' => t('An existing local file system path for storing private cloud files. It should be writable by Drupal and not accessible over the web. See the online handbook for <a href="@handbook">more information about securing private files</a>.', array('@handbook' => 'http://drupal.org/documentation/modules/file')),
 '#after_build' => array('system_check_directory'),
 '#weight' => 1,
 );

 // rearrange form
 if (isset($form['actions'])) {
 $form['actions']['#weight'] = 50;
 }
 if (isset($form['file_default_scheme'])) {
 $form['file_default_scheme']['#weight'] = 10;
 }
}

El nombre del campo se utilizara para crear una nueva variable de drupal que luego podemos obtener usando la función variable_get().

Luego de implementar el anterior código e ingresando a la página admin/config/media/file-system tendremos una nueva sección similar al presentado en la siguiente figura.

cloud settings 0

  1. Registrar URL para manejar la descarga de los archivos.

Cuando se registro la clase CloudStreamWrapper se definió que la URl para los archivos guardados en el stream seria system/cloud/xxxx esta URL por supuesto no existe y deberemos registrarla por medio de la implementación del hook hook_menu como se muestra a continuación.

/**
 * Implements hook_menu().
 */
function MIMODULO_menu() {
 $items = array();

 $items['system/cloud'] = array(
 'title' => 'File download',
 'page callback' => '_cloud_stream_file_download',
 'page arguments' => array('cloud'),
 'access callback' => TRUE,
 'type' => MENU_CALLBACK,
 );

 return $items;
}

De esta forma cuando se intente descargar un archivo del strem wrapper Cloud se llamara a la función _cloud_stream_file_download para su procesamiento.

  1. Crear función para implementar la descarga.

La función que procesa las descargas es muy parecida a la función estándar de Drupal para descargar archivos, en donde se hace el chequeo de si los usuarios tienen derechos para descargar el archivo, como se presenta a continuación.

/**
 * File download for dynamic files
 */
function _cloud_stream_file_download() {
 // Merge remainder of arguments from GET['q'], into relative file path.
 $args = func_get_args();

 $scheme = array_shift($args);
 $target = implode('/', $args);
 $uri = $scheme . '://' . $target;

 if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
 $headers = _private_dynamic_stream_file_download_headers($uri);
 if (count($headers)) {
 _cloud_stream_file_transfer($uri, $headers);
 }

 // DENY if no headers
 drupal_access_denied();
 }
 else {
 drupal_not_found();
 }
 drupal_exit();
}

Pero en la función anterior en lugar de llamar a la función estándar de Drupal para transferir el archivo, llamaremos a una segunda función llamada _cloud_stream_file_transfer donde agregaremos algo particular del stream wrapper.

Además debemos crear la función _private_dynamic_stream_file_download_headers que genera los headers para poder descargar el archivo.

function _private_dynamic_stream_file_download_headers($path){
  $filename = array_pop(explode('//', $path));
  return array('Content-Type' => 'application/octet-stream',
  'Content-Disposition' => 'attachment; filename="' . $filename . '"',
  'Content-Length' => filesize($path));

}
  1. Crear función para implementar la transferencia de los archivos.

/**
 * Transfers a cloud file to the client using HTTP or HTTPS.
 *
 * Pipes a dynamic file through Drupal to the client.
 *
 * @param $uri
 * String specifying the file URI to transfer.
 * @param $headers
 * An array of HTTP headers to send along with file.
 */
function _cloud_stream_file_transfer($uri, $headers) {
 if (ob_get_level()) {
 ob_end_clean();
 }

 // extract scheme
 $scheme = file_uri_scheme($uri);
 $stream_wrapper = _cloud_stream_get_wrappers($scheme);

 // Build appended data
 $append_content = NULL;
 if ($stream_wrapper && !empty($stream_wrapper['cloud'])) {
 $append_data = module_invoke_all('cloud_stream_append', $uri);

 if (!empty($append_data)) {
 // combine all data
 $append_content = implode('', $append_data);

 // Add the appended length to the specified headers
 if (isset($headers['Content-Length'])) {
 $headers['Content-Length'] += strlen($append_content);
 }
 }
 }

 // add headers and send file headers
 foreach ($headers as $name => $value) {
 drupal_add_http_header($name, $value);
 }

 // send remaining headers
 drupal_send_headers();

 // Transfer file in 1024 byte chunks to save memory usage.
 if ($scheme && file_stream_wrapper_valid_scheme($scheme) && $fd = fopen($uri, 'rb')) {
 while (!feof($fd)) {
 print fread($fd, 1024);
 }
 fclose($fd);

 // Append content
 if (isset($append_content)) {
 print $append_content;
 }
 }
 else {
 drupal_not_found();
 }
 drupal_exit();
}

En la función anterior se implementa el hook_cloud_append_data, este hook permitirá a otros módulos detectar que tipo de archivo se esta descargando y optar o no por agregar algo de contenido al final. Esta funcionalidad podría ser útil para agregar una firma a documentos PDF o TXT, otras implementaciones se dejan a la imaginación del lector.

Ademas en la función anterior se utilizo la función _cloud_stream_get_wrapper para determine que schema o stream se esta usando en el archivo, como se muestra en el siguiente código.

/**
 * Returns stream wrapper info for a specific scheme
 */
function _cloud_stream_get_wrappers($scheme) {
 $wrappers = file_get_stream_wrappers();
 return isset($wrappers[$scheme]) ? $wrappers[$scheme] : NULL;
}

Llegado a este punto ya la implementación de un stream wrapper personalizado para Drupal 7 esta terminada.

Ahora cuando se cree un field tipo File en cualquier tipo de contenido se podra escoger en que stream wrapper privado se quiere almacenar y el strean wrapper cloud estar disponible, como se muestra en la siguiente imagen.

privite destination

Espero que haya sido de su agrado.

Recibe consejos y oportunidades de trabajo 100% remotas y en dólares de weKnow Inc.