miércoles, marzo 07, 2007

Actualización de Medidor de progreso de subida de archivos en Ajax

Este artículo es la traducción del este PHP AJAX File Upload Progress Meter Updates

Durante el fin de semana mi código de medidor de subida de archivos tuvo mucho tráfico. Parece que está en la lista de populares de del.icio.us y tambien está obteniendo un montón de votos en digg. Para celebrar esto actualicé el código.
La nueva característica mas importante es que devuelve información sin tener que parchear PHP. Ahora la versión parcheada te da mas información como velocidad de subida y tiempo estimado de finalización. Pero igual damos devolución al usuario sin usarlo.
También creé algunas páginas wiki para empezar el proceso de documentación
Acá están los demos:
Con el Parche y la extensión
Sin el parche
Para tener la velocidad de subida vas a necesitar descargar e instalar el parche y extensión PHP Upload Progress.

Si queres usar el codigo necesitas:
Instalar HTML_AJAX (pear install HTML_AJAX-alpha)
Descargar PAFUPMU e instalarlo en algún lugar accesible.
Agregar el código a tu página usando demo.php como un ejemplo.

La forma básica en que trabaja el código es: tomamos un formulario que contiene una subida de archivo y un aceptar con un objetivo (target) en un iframe oculto. Hacer esto permite que la subida pase en el fondo. Entonces consultamos al servidor en lapsos regulares (digamos intervalos de 2 segundos) preguntando para actualizar el estado. Si tenés el parche+extensión instalado te da la velocidad de subida del archivo, etc. Si no tenés la extensión paramos de hacer llamadas AJAX y solo animamos la barra de estado hasta que la subida termine en el iframe y nos diga que terminamos.
En demo.php nos encargamos de generar la página vigente y manejamos la subida en la misma página. Entonces empezamos gestionado la actualización.

<?php
include ‘UploadProgressMeter.class.php’;
$fileWidget = new UploadProgressMeter();
if ($fileWidget->uploadComplete()) {
// output javascript to the iframe to send a final status to the main window
// this will catch error conditions
echo $fileWidget->finalStatus();
// move the file(s) where they need to go
exit;
}
?>

Este código solo funciona cuando estas subiendo por eso no veras nunca una salida de este. Esto puede hacer problemático el seguimiento de errores. Por otro lado esto se puede resolver haciendo el iframe no oculto.
Luego montas el Javascript necesario, primero HTML_AJAX y después los subidores JS.

<script type=’text/javascript’ src=’demoserver.php?client=main,request,httpclient,dispatcher,json,util’></script>
<script type=’text/javascript’ src=’demoserver.php?stub=UploadProgressMeterStatus’></script>
<?php echo $fileWidget->renderIncludeJs(); ?>

demoserver.php está incluido en la fuente, puede ser que quieras nombrarlo de otra manera si realmente usas este código.Es solo una página usando HTML_AJAX_Server para registrar la clase UploadProgressMeterStatus.
También necesitás algo de CSS para darle estilo a la barra de progreso. Lo único importante es que .progressBar y . progressBar.bar estén posicionadas relativamente.

.progressBar {
position: relative;
padding: 2px;
width: 300px;
height: 40px;
font-size: 14px;
}
.progressBar .background {
border: solid 1px black;
width: 270px;
height: 20px;
}
.progressBar .bar {
position: relative;
background-color: blue;
width: 0px;
height: 20px;
}

Finalmente termina las cosas construyendo un formulario. Fijate que podés incluir otros elementos en el formulario, pero si necesitan producir una salida vas a tenés que manejar rellamadas en la pagina padre desde la salida del iframe.

<form action="demo.php" enctype="multipart/form-data" method="post" <?php echo $fileWidget->renderFormExtra(); ?>>
<?php echo $fileWidget->renderHidden(); ?>
<label>Select File: render(); ?>
< ?php echo $fileWidget->renderProgressBar(); ?>
</div>
</form>

martes, marzo 06, 2007

Progreso del upload de un archivo en AJAX

Este artículo es la traducción del este AJAX File upload Progress

El ejemplo de como usar la versión mas nueva está disponible en otro post.
Actualización
Hay una nueva versión de este código que sigue la misma premisa pero usa HTML_AJAX en vez de JPspan.
Podes ver el nuevo demo y ver el código en websvn.
Notar también que el servidor no está configurado para aceptar archivos mayores de 8 Mb. Si es así el script va a fallar.
También estoy buscando a alguien que me ayude a mejorar el manejo de errores, si estas interesado en participar y hacer algunas actualizaciones de código nuevo házmelo saber.

Sigue el artículo Original

Un par de días atrás, encontré un proyecto interesante de ruby on rails. Usa Ajax para actualizar una barra de progreso mientras sube un archivo. Este truco es un parche para rails para obtener el estado de subida y hacer la subida en un iframe para que la pagina principal siga activa.
Entonces para implementar esto solo tenía que encontrar el parche que provee el estado de la subida en PHP y luego implementarlo en mi pequeño iframe accesorio de subidas.
Encontré el código PHP con un poco de trabajo en Google: Barra de progreso de subida
Primero necesitas instalar el parche y la extensión, las instrucciones incluidas son fáciles de seguir. El único problema que encontré es que hay que configurar: upload_progress_meter.store_method = “file” en mi php.ini en primera instancia antes de probar los scripts.
También tuve un problema con JPSpan, si estas teniendo problemas de red el llamado de estado puede tomar mas de un segundo y vas a tener un alerta de error de llamada en progreso. Esto puede ser resuelto con la versión actual de JPSpan pero me gustaría ver alguna api agregada para ayudar. Los objetos con Proxi necesitan algún tipo de llamada en progreso para hacer un arreglo fácil.
Un ítem notable es que la extensión provee información de la transferencia total de manera que solo se implementa una barra por formulario sin importar cuantos archivos tenga. El código Javascript esta programado para varios formularios en una página y que suban al mismo tiempo, pero no ha sido testeado.
Acá está el demo que estabas esperado, para la mayoría de la conexiones un archivo de 250k va a ser suficiente para apreciar algo además de conectar y completar.
Además si alguno tiene el tiempo y las habilidades para revisar el parche php y ver que necesitaría para estar integrado,por favor haganmelo saber. No se nada del autor por eso no sé por que no está integrado pero parece medio loco tener un parche de 3k tan útil solamente disponible para aquellos que estar dispuestos a parchear.

Detalle del código
EL flujo básico sucede así:
Muestra una página con un formulario:
Esta página tiene un iframe oculto, un div de progreso oculto, y algun código javascrip extra.
Selección del archivo para subir y acepto el fomulario:
El formulario tiene un objetivo (target) en el iframe oculto, por esto aún cuando icono indicador de actividad del navegador empiece funcionar, la pagina principal no va a tener nuevo contenido cuando el upload haya terminado.
El evento onclick del formulario dispara una función determinada:
La función encuentra el div de progreso y lo muestra, también registra una función para actualizar el estado cada segundo.
La función actualización dispara cada segundo:
Esta función controla un contador para ver si hay algun div mas para actualizar, si el contador es cero detiene la función actualización desde el disparo otra vez.
La función crea un objeto proxy remoto para la clase php UploadProgressMeterStatus si aún no ha sido creado.
La función llama al método get_status en el objeto proxy con un lista de todos los divs de progreso y una de los UPLOAD_IDENTIFIER que necesitamos los estados.
La función existe.
El método get-status en la clase de php es llamado.
El método llama a upload_progress_meter_get_info() para cada uno pasado en el indentificador, la información es formateada a un porcentaje y un mensaje que es devuelto.
La funcion de rellamada para get_status se llama cuando laclases PHP devuelve información.
La rellamada actualiza el div de progreso.
Si estubiera al 100% decrementamos nuestro contador de div de progreso y lo removemos de la lista de divs para ser actualizados.
El iframe puede tambien cargar una página una vez que la carga haya terminado, actualmente no hace nada.
Código de ejemplo
Este es realmente el único código PHP interesante en el proyecto, y no tan interesante. Cuando tenés instalada la extensión de medidor de progreso de subida todo formulario que este haciendo una subida de archivo y tenga una barra oculta que se llame UPLOAD_IDENTIFIER puede ser seguido. El identificador tiene que ser pasado en la función upload_progress_meter_get_infopor eso tenés que hacer el seguimiento del mismo del lado de Javascript. Acá solo pasamos eso identificadores, hacemos un grupo de ellos en el array de resultado y devolvemos los resultados. Notar que el array devuelto no esta documentado en ningún lado por lo que este código y el código en el ejemplo de php provisto con la extensión es el mejor lugar para empezar si querés.

?php

/**
* Obtener el estado de todas las subidas que se pasen.
*/
function get_status($ids) {
$ret = array();
foreach($ids as $id => $upId) {
$ret[$id] = new stdClass();

$tmp = upload_progress_meter_get_info($upId);
if (!is_array($tmp)) {
$ret[$id]->message = “Complete”;
$ret[$id]->percent = “100″;
break;
}

if ($tmp[‘bytes_total’] < percent =" 100;" percent =" round($tmp[‘bytes_uploaded’]" percent ="=">message = “Complete”;
}

$eta = sprintf(“%02d:%02d”, $tmp[‘est_sec’] / 60, $tmp[‘est_sec’] % 60 );
$speed = $this->_formatBytes($tmp[’speed_average’]);
$current = $this->_formatBytes($tmp[‘bytes_uploaded’]);
$total = $this->_formatBytes($tmp[‘bytes_total’]);

$ret[$id]->message = “$eta left (at $speed/sec) $current/$total($percent%)”;
$ret[$id]->percent = $percent;
}
return $ret;
}
?>

En el lado de JavaScript hay un poco mas de código, pero no es para nada complejo.Este código es usado para mostrar la barra de progreso y darle algunos valores iniciales. Las cosas mas importantes para resaltar son que hay agregado un método de actualización al div, este es un lindo truco por que permite la extensión en tiempo de ejecución de los objetos en DOM, y va a hacer actualizar las cosas agradable y fácil en otras funciones. También agregamos un método getFirstDivByClass al div, hago esto para no tener tantos div para seguir, las clases solo tienen que ser únicas dentro de la barra de proceso para que ande y eso es mucho mas fácil de lograr

/**
* Muestra una barra de progreso y la establece a 0
*/
function UploadProgressMeter_EnableProgress(progress_id) {
var progress = document.getElementById(progress_id);
progress.style.display = ‘block’;
progress.percent = 0;
progress.message = "Connecting";

progress.update = function() { this.getFirstDivByClass(‘bar’).style.width = this.percent+‘%’; this.getFirstDivByClass(‘message’).innerHTML = this. message; }

progress.getFirstDivByClass = function(className) {
var nodes = this.getElementsByTagName(‘div’);
for(var i = 0; i < classname ="="">

El código siguiente llama a un proxy remoto y crea una función de rellamada para gestionar el resultado. Hay un área donde se pueden hacer mejoras. Primero debería haber un control de si hay actualmente una llamada en progreso. Luego, sería inteligente llamar al servidor menos (especialmente en archivos grandes) y solo generar las estadísticas del índice de descarga actual.Esto va a agregar un poco de complejidad pero permitiría a la barra de progreso actualizar suavemente y permitiría que las llamadas al servidor bajar a una vez cada 5 o 10 segundos
Si no haces mucha programación javascript no vale la pena el uso de for(var prop in result) en delete UploadProgressMeter_active[prop];
for(var prop in result) es como iteras en las propiedades de un objeto, esto te permite usarlas como arreglos asociativos (solo mira los métodos en los objetos ya que vas a iterar en ellos también).
delete UploadProgressMeter_active[prop] es el equivalente de unset($array[’key’]);

/**
* Actualizar las barras de progreso de todas las barras vigentes
*/
function UploadProgressMeter_Update() {
if (UploadProgressMeter_count == 0) {
clearInterval(UploadProgressMeter_intervalId);
UploadProgressMeter_intervalId = false;
return;
}

if (UploadProgressMeter_remote == false) {
var callback = {
get_status: function(result) {
for(var prop in result) {
if (prop != "toString") {
document.getElementById(prop).percent = result[prop].percent;
document.getElementById(prop).message = result[prop].message;
document.getElementById(prop).update();

if (document.getElementById(prop).percent == 100) {
UploadProgressMeter_count–;
delete UploadProgressMeter_active[prop];
}
}
}
}
}
UploadProgressMeter_remote = new uploadprogressmeterstatus(callback);
}
UploadProgressMeter_remote.get_status(UploadProgressMeter_active);
}

Code List

Actualizaciones
Te habras dado cuenta que el demo dejo de andar un par de veces. Esto está relacionado con dos cosas y hay cosas que puede ser que quiera pensar si vas a usar el parche. Primero escribe archibos tmp y falla sileciosamente si no existe más (scripts que limpian los tmp). Segundo, esta en my servidor php5 que tiene algunos usuarios que estan presionando para que me actualice a php 5.1 beta y me olvide de reparchear. Reparchear no fue un gran problema aunque tube que mover donde una funcion fue declarada para obtener la extensión para compilar en gcc 4 (me actualice a fedora core 4 también)
De todos modos el demo está trabajando y debería seguir trabajando siempre y cuando me acuerde de reparchear con cada actualización.
Nueva versión
Este código ha sido actualizado para trabajar con HTML_AJAX y para manejar mejor las condiciones de error. No he hecho un lanzamiento oficial paro podes tomarlo del svn.
Ver en: http://svn.bluga.net/HTML_AJAX/UploadProgressMeter/trunk/
O usar websvn para obtener un tarball: http://websvn.bluga.net/wsvn/HTML_AJAX/UploadProgressMeter/trunk/?rev=0&sc=0
También si estas interesado en ayudar con la medición de progreso de subida, haganmelo saber.