Una gran característica de S3 es que puedes subir directamente a tu bucket a través de una acción post, el flujo es muy simple, solo necesitas generar algunos parámetros de la ubicación donde se almacenará y los permisos/meta-información, con el SDK de Amazon para PHP, puedes hacerlo fácilmente con:
require_once 'sdk.class.php'; $s3_uploader = new S3BrowserUpload(); $params = $s3_uploader->generate_upload_parameters($bucket, $expires, array( 'acl' => AmazonS3::ACL_PUBLIC, 'key' => $path_file, 'Content-Type' => $mime, ));
El resultado de $params
contendrá un arreglo cuyo índice $params['input']
necesita ser pasado a valores ocultos de una forma y finalmente necesitas agregar un tipo file
donde el usuario seleccionará el archivo (al final de los otros parámetros):
<input type="file" name="file" />
También puedes especificar la dirección URL donde regresará el navegador después que el usuario haya subido el archivo.
Esto trabaja muy bien para subir archivos de “forma tradicional”, pero ¿cómo lo harías si quisieras tomar ventaja de las nuevas API’s de HTML5 como almacenar una cadena en un archivo; o obtener la imagen de un canvas y almacenarlo directo en S3 sin enviarlo primero a un servidor; o hacer una miniatura en el cliente y subirla directo a S3?
El primer problema aquí es que no puedes hacer llamadas crossdomain (interdominio), debido a que S3 no tiene la característica de agregar una cabecera de Access-Control-Origin
que permitiría enviar el archivo de forma directa, afortunadamente con postMessage
puedes subir un archivo HTML y uno javascript a tu cuenta S3 y funcionará mediante un “proxy”.
El otro problema es que postMessage
solo permite tipos que puedan ser clonados, por seguridad File
/FileReader
no pueden ser clonados por lo que necesitas pasar el contenido del archivo directamente en lugar del objeto.
Este ejemplo te permitirá
- Seleccionar una imagen
- Reducirla a la mitad de tamaño mediante un canvas
- Obtener los datos binarios codificados en base64
- Pasar mediante un
postMessage
el contenido - Decodificar los datos en base64
- Pasarlo mediante AJAX a tu cuenta de S3
Esto es simple debido a que se están utilizando características estándar; pero desafortunadamente como HTML5 es un estándar en progreso, muchos navegadores aún no lo soportan, por el momento lo soportan Firefox 4+ and Chrome 11+
Primero revisaremos la parte que será almacenada en tu bucket de S3 y servirá como proxy, tendremos estos tres archivos:
index.html
Contendrá solo código para cargar el javascript y nada másbase64-binary.js
Es un archivo útil que permite decodificar la imagen en base64 a unArrayBuffer
que será convertido en unBlob
y será enviado como un “archivo” en una acción POSTupload.js
Este archivo contendrá la lógica que recibirá el mensaje y convertirá la imagen codificada en base64 con parámetros POST y lo convertirá en una petición AJAX
Puedes ver todos los archivos aquí:
https://github.com/danguer/blog-examples/tree/master/html5/s3/direct-upload/s3-files
La parte principal de upload.js
es:
window.addEventListener("message", function(event) { var data = event.data; //obtener los datos del mensaje //subir el archivo mediante blob var separator = 'base64,'; var index = data.content.indexOf(separator); if (index != -1) { var bb = new BlobBuilder(); //decodificar los datos de Base64 y convertirlos a un ArrayBuffer var barray = Base64Binary.decodeArrayBuffer(data.content.substring(index+separator.length)); bb.append(barray); var blob = bb.getBlob(); //pasar los parámetros POST mediante FormData var formdata = new FormData(); for (var param_key in data.params) { formdata.append(param_key, data.params[param_key]); } formdata.append("file", blob, "myblob.png"); //agregar como Blob //finalmente enviar los datos mediante AJAX var xhr = new XMLHttpRequest(); xhr.open("POST", data.url, true); xhr.send(formdata); } }, false);
Como puedes ver solamente estamos decodificando el base64 y convirtiéndolo a un Blob
, pasando todos los parámetros mediante FormData
y enviándolos mediante AJAX.
Como este archivo estará almacenado en S3 no habrá ningún problema de crossdomain, pero necesitas poder crear un Blob
y un FormData
para que funcione.
En el script para cargar el archivo tendremos un poco más de código, primero necesitamos código para generar los parámtros de S3, esto se puede hacer fácilmente en PHP con::
<?php //iniciar aws-sdk require_once 'sdk.class.php'; //la respuesta será en JSON header('Content-Type: application/json'); $bucket = 'danguer-blog'; //tu bucket en s3 $bucket_path = 'upload-html5/tmp'; //"directorio" donde se guardarán los archivos $filename = isset($_POST['name'])?$_POST['name']:null; $filemime = isset($_POST['mime'])?$_POST['mime']:null; //manejar errores if (empty($filename)) { print json_encode(array( 'error' => 'debe enviar el nombre', )); return; } if (empty($filemime)) { print json_encode(array( 'error' => 'debe enviar el mime', )); return; } if (strpos($filename, '..') !== false) { print json_encode(array( 'error' => 'no se aceptan rutas relativas', )); return; } $expires = '+15 minutes'; //el token solo será válido por 15 minutos $path_file = "{$bucket_path}/{$filename}"; $mime = $filemime; //mandar el contenido mime para ayudar a los navegadores //obtener los parámetros de S3 para subir directamente $s3_uploader = new S3BrowserUpload(); $params = $s3_uploader->generate_upload_parameters($bucket, $expires, array( 'acl' => AmazonS3::ACL_PUBLIC, 'key' => $path_file, 'Content-Type' => $mime, )); print json_encode(array( 'error' => '', 'url' => "http://{$params['form']['action']}", 'params' => $params['inputs'] )); return;
Después necesitamos presentar la entrada del archivo y crear un iframe que cargue los archivos S3 que hemos subido:
<!-- hiden frame --> <iframe id="postMessageFrame" src="<?=$url_iframe?>"> </iframe> <h3>Subir archivos</h3> <input type="file" accept="image/*" onchange="uploadFile(this.files)">
Y el código javascript es un poco más, debido a que una vez que el usuario seleccione un archivo, necesitamos cambiarle el tamaño
function resizeImage(file, mime) { var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); var canvasCopy = document.createElement("canvas"); var ctxCopy = canvasCopy.getContext("2d"); var reader = new FileReader(); reader.onload = function() { var image = new Image(); image.onload = function() { //escalar la imagen a la mitad canvasCopy.width = image.width; canvasCopy.height = image.height; ctxCopy.drawImage(image, 0, 0); canvas.width = image.width * 0.5; canvas.height = image.height * 0.5; ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height); //convertirlo en imagen y obtener el binario codificado en base64 //para enviarlo mediante postMessage var url = canvas.toDataURL(mime, 0.80); uploadImage(file, url) } image.src = reader.result; }; //leer contenidos reader.readAsDataURL(file); }
Como puedes ver estamos abriendo el archivo seleccionado, lo dibujamos en un canvas, lo copiamos en otro canvas a la mitad de su tamaño y obtenemos su representación binaria en base64 con el mismo mimetype del original, finalmente llamaremos para subir la imagen:
function uploadImage(file, dataURL) { //cargar firma para S3 var signature_params = { mime: file.type, name: file.name }; $.post(url_ajax_signature, signature_params, function(response) { //enviar mediante postMessage var windowFrame = document.getElementById('postMessageFrame').contentWindow ; var data = { params: response.params, url: response.url, content: dataURL } //enviar los datos de la firma de S3 y los datos binarios en base64 windowFrame.postMessage(data, 'http://<?=$url_iframe_host?>'); }, 'json'); }
Primero obtenemos los parámetros POST para S3 mediante AJAX del archivo PHP y después enviamos esos datos mediante postMessage
al iframe en S3 para que pueda procesarlo y cargarlo a S3 sin necesidad de primero enviar el archivo al servidor y de ahi cargarlo a S3, con esto podemos cargar directamente a S3 desde el navegador del cliente y si es necesario podemos verificar los contenidos para evitar que se suba archivos maliciosos.
Todo el código se puede ver aquí:
https://github.com/danguer/blog-examples/tree/master/html5/s3/direct-upload