Gestor de descargas
[cSharpDownload V1.0 beta]

Fecha:
Autor: Unai Zorrilla Castro Escríbeme

 
.

En anteriores colaboraciones se expusieron los métodos para crear Controles de Usuario para embeber en Web ( 1º Parte  2º Parte) en estos se trato tanto la implementación mediante UserControl estándar o expuestos como objetos COM, lo que nos posibilitaba que pudiéramos lanzar eventos al cliente Web.
Para terminar con este tema he considerado la posibilidad de crear una utilidad para comprobar la potencia de esta tecnología. Con el fin de hacer algo útil he decidido crear un control para gestionar las descargas de un sitio web. Mediante este control de usuario los visitantes de una web podrán gestionar la descarga de múltiples archivos sin necesidad de ningún tipo de instalación.

De entre los múltiples NameSpaces que nos ofrece .NET System.Net nos ofrece un conjunto de clases apropiada para la realización de Request a Uri. Este control se fundamenta en la clase DownFiles para realizar la mayor parte del proceso. Dicha clase no solo gestiona las llamadas a los documentos a descargar sino que gestiona la escritura de dichos archivos en el cliente y lanza una serie de eventos que son manejados desde el control de usuario con el propósito de que este muestre información al cliente.
Un ejemplo de este control se encuentra al final del artículo, básicamente consiste en agregar mediante javascript los uri, nombres y tamaños de los ficheros a descargar y posteriormente asignar estas cadenas al control. En las siguientes imágenes vemos el control en funcionamiento ( Ver figura 1.1).

Figura 1.1
 

Implementación de DownFiles :

using System;
using System.Net;
using System.IO;
using System.Security;
using System.Security.Permissions;
using System.Threading;
using CommonFolderDialog;
using System.Windows.Forms;
namespace csharpdownfiles
{
/// <summary>
/// DownFiles es una clase para gestionar la descarga de archivos a traves de Uri's.
/// *******************************************************************************
/// *Creada por Unai Zorrilla Castro molotess@hotmail.com
/// *
/// *05-01-2004 para colaboración con el 'Guille
/// ********************************************************************************
/// </summary>

public class DownFiles : IDisposable
{
#region Delegados
public delegate void HandlerDownFiles();
public delegate void HandlerNewFileDown(string Name,DownState State,double Tam);
#endregion
#region Eventos
public event HandlerDownFiles OnCancel;
public event HandlerDownFiles OnInit;
public event HandlerDownFiles OnReiniciar;
public event HandlerDownFiles OnFin;
public event HandlerDownFiles OnPause;
public event HandlerNewFileDown OnDown;
public event HandlerNewFileDown OnFailedDown;
#endregion
#region Miembros privados de la clase
private string _FilesToDown;
private string _FilesLenghtDown;
private string _NameFiles;
private public _IsPause;
private public _IsInit;
private string[] _ArrayFiles;
private string[] _ArrayNameFiles;
private string[] _ArrayLenghtFiles;
private int _position;
private FileStream fs;
private string _PathFiles;
private Thread Threadown;
#endregion
#region Atributos de la case
/// <summary>
/// <returns>Devuelve el string con la coleccion de archivos para descargar</returns>
/// </summary>

public string FilesToDown
{
get
{
return this._FilesToDown;
}
set
{
this._FilesToDown = value;
}
}
public string NameFiles
{
get
{
return this._NameFiles;
}
set
{
this._NameFiles = value;
}
}
public string FilesLenghtDown
{
get
{
return this._FilesLenghtDown;
}
set
{
this._FilesLenghtDown = value;
}
}
/// <summary>
/// <returns>Indica si la descarga esta pausada</returns>
/// </summary>

public IsPause
{
get
{
return this._IsPause;
}
set
{
this._IsPause = value;
OnPause();
}
}
#endregion
#region Enumeradores
public enum DownState
{
Descargado =0,
Fallido
};
#endregion
/// <summary>
/// Constructor de la clase;
/// </summary>

public DownFiles()
{
}
/// <summary>
/// Dispose de la clase DownFiles
/// </summary>

public void Dispose()
{
if(fs != null)
fs = null;
}
/// <summary>
/// Destructor
/// </summary>

~DownFiles()
{
this.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// Indexador de solo lectura
/// <returns><item>Devuelve el archivo espeecificado por la posicion</item></returns>
/// </summary>

[Obsolete("Indexador no utilizado, implementacion para primera Beta",true)]
public string this[int Position]
{
get
{
if(Position > this._ArrayFiles.Length)
{
throw new DownFilesExceptionB();
}
else
{
try
{
return this._ArrayFiles[Position];
}
catch
{
throw new DownFilesExceptionA();
}
}
}
}
#region Metodos de la clase
/// <summary>
/// Inicialización de la clase
/// </summary>

public void IniciarComponente()
{
//Creamos el array de archivos a descargar
if(this._FilesToDown.Equals(""))
throw new DownFilesExceptionA();
else
{
this._IsInit = true;
this._position = 0;
this._ArrayLenghtFiles = this._FilesLenghtDown.Split(';');
this._ArrayFiles = this._FilesToDown.Split(';');
this._ArrayNameFiles = this._NameFiles.Split(';');
if((this._ArrayFiles.Length != this._ArrayLenghtFiles.Length) || (this._ArrayFiles.Length != this._ArrayNameFiles.Length) || (this._ArrayLenghtFiles.Length != this._ArrayNameFiles.Length))
{
throw new DownFilesExceptionA();
}
OnInit();
Threadown = new Thread(new ThreadStart(Descargar));
Threadown.Priority = ThreadPriority.Lowest;
Threadown.Start();
}
}
private void Descargar()
{
try
{
DownloadFiles(this._ArrayFiles,this._position);

}
catch(ThreadAbortException )
{
OnCancel();
}
}
/// <summary>
/// Método publico para descarga de archivos
/// </summary>
/// <param name="Files"></param>

private void DownloadFiles(string[] Files,int Position)
{
WebClient wbc = new WebClient();
byte[] docbyte;

//Pedimos el directorio donde desea guardar los documentos y solicitamos los permisos
SelectDir seldir = new SelectDir();
if(seldir.ShowDialog() == DialogResult.OK)
{
_PathFiles = seldir._FullDirPath;
}
FileIOPermission fiopp = new FileIOPermission(FileIOPermissionAccess.Write,_PathFiles);
try
{
fiopp.Demand();
}
catch(SecurityException)
{
MessageBox.Show("Usted no tiene permisos suficientes para guardar \n los documentos en su disco","Info",MessageBoxButtons.OK,MessageBoxIcon.Information);
OnCancel();
return;
}
//****************


for( int i= Position ; i<=Files.Length -1 ; i++)
{
try
{
docbyte = wbc.DownloadData(Files[i]);
fs = new FileStream(_PathFiles + @"\" + this._ArrayNameFiles[i],FileMode.Create,FileAccess.Write); fs.Write(docbyte,0,docbyte.Length); fs.Close(); //Soltamos un evento para indicarle que un archivo ya ha sido descargado OnDown(this._ArrayNameFiles[i],DownState.Descargado,Convert.ToDouble(this._ArrayLenghtFiles[i])); } catch { //Soltamos un evento para decirle que un archivo no ha podido descargarse OnFailedDown(this._ArrayNameFiles[i],DownState.Fallido,Convert.ToDouble(this._ArrayLenghtFiles[i])); }
if(IsPause) { OnReiniciar(); Threadown.Suspend(); } } //Si esta terminado no en pausa,lanzamos el evento que indica que la descarga ha finalizado OnFin(); }
/// <summary>
/// Reinicia el proceso de descarga
/// </summary>
public void Reiniciar() { if(!this._IsInit) throw new DownFilesExceptionC(); else Threadown.Resume(); }
/// <summary> /// Cancela el proceso de descarga /// </summary> public void Cancelar() { if(!this._IsInit) throw new DownFilesExceptionC(); else { Threadown.Abort(); OnCancel(); } }
#endregion } #region ClasesDeExcepciones
public class DownFilesExceptionA : System.Exception { public DownFilesExceptionA() : base(@"Excepción no controlada, no se encontraron archivos.Tambien es posible que el numero de archivos y nombres o tamaños no coincida") { } }
public class DownFilesExceptionB : System.Exception { public DownFilesExceptionB() : base(@"Indice fuera del rango del array de archivos") { } } public class DownFilesExceptionC : System.Exception { public DownFilesExceptionC():base(@"La descarga aún no fue iniciada, no se puede reiniciar o abortar") { } } #endregion }

La clase WebClient proporciona métodos para realizar Request a Uri conocidos y gestionar los Response. He obtado por el uso de hilos para liberar un poco al procesador en la lectura y escritura en E/S. Pueden probar a gestionar estas descargas y escrituras en disco sin uso de hilos y observarán como se sobrecarga en exceso el uso del procesador. El uso de eventos en esta clase es una buena manera para comunicarse con el control de Usuario con el fin de que este muestra la información adecuada al cliente. Se ha implementado un destructor y la inteface IDisposable con el fin de asegurarse que no queda ninguna referencia a un archivo. En esta versión no esta implementada pero podría ser interensante el uso de referencias débiles ( WeakReference ) con el fin de poder reanudar una descarga aunque se haya llamado al Dispose y el GarbageCollector procediera con una recolección.

Puesto que en C# no existe una forma directa para seleccionar directorios y con la premisa de no usar muchos ensamblados externos me he decidido por implemetar un selector de directorios, la clase que gestiona esta tarea es SelectDir y su proceo es bastante sencillo e intuitivo, básicamente se empieza con un array de posibles identificadores de volúmes y mediante las clases DirectoryInfo y el método Exists() se va construyendo un árbol de directorios (Ver figura 1.2).

Figura 1.2


Un aspecto a tratar en la creación de controles de usuario es el tema de la seguridad, .NET Framework establece para los ensamblados un nivel de seguridad basado en evidencias (básicamente estas indican la procedencia del ensamblado ) para un control de este tipo .NET Framework establece la evidencia de "Internet".Como se puede observar en la Figura 1.3 esta no contiene uno de los permisos necesarios para poder ejecutar nuestro gestor de descargas puesto que necesitamos de acceso al disco. De hecho dentro de Downfiles se solicita este permiso

...

FileIOPermission fiopp = new FileIOPermission(FileIOPermissionAccess.Write,_PathFiles);
try
{
fiopp.Demand();
}
catch(SecurityException)
{
MessageBox.Show("Usted no tiene permisos suficientes para guardar \n los documentos en su disco","Info",MessageBoxButtons.OK,MessageBoxIcon.Information);
OnCancel();
return;
}
//****************

Figura 1.3  Figura 1.4

 

Se hace por lo tanto necesario que el cliente otorgue los permisos necesarios para poder llevar a cabo esta tarea ( en este punto recomiendo la lectura de las colaboraciones anteriores ). .Net Framework dispone de una herramienta conocida como PERMVIEW (Figura 1.4 ) la cual permite ver los permisos mínimos, opcionales y aquellos que nunca debieran concederse a un ensamblado. Para poder establecer estos permisos añadimos el atributo PermissionSet a nuestro ensamblado de la siguiente forma.


[assembly: PermissionSetAttribute(SecurityAction.RequestMinimum,Name="FullTrust")]


Asignarle FulTrust parece algo excesivo aunque facilita la tarea del cliente para configurar el ensamblado. De cualquier forma se podría desarrollar un script de Windows para asignar permisos a un ensamblado en particular y así liberar al cliente de procesos de configuración que puede que no sepa llevar a cabo.

Nota.

Se adjuntan dos archivos ZIP, uno con el código fuente y un archivo con todo lo necesario para poder ver un ejemplo del control. Para no hacer crecer mucho el contenido no he introducido los documentos a los cuales se hace referencia desde el ejemplo del control para descargar (archivo descargar.html)pero basta con crear los doc1.pdf,doc2.pdf,doc3.pdf al lado de los archivos descargas.html y csharpdownload.html. Estos archivos junto con la carpeta librería han de situarse en el directorio wwwroot. Una vez colocados en el directorio solo queda concederle los permisos al ensamblado para ello y con el fin de testearlo podéis ir a Herramientas Administrativas->Asistente de Microsoft NET Framework seleccionar Ajustar la seguridad, Realizar cambios en este equipo y estableces máxima confianza para Intranet local. Una vez testeado el control podéis restablecer vuestra configuración.
Si te ha gustado la colaboración... puedes votarla en el panel de PanoramaBox. Gracias!!


ir al índice

Fichero con el código fuente (unai_csharpdownfiles_src.zip - 66.8 KB)

Fichero con el Ejemplo (unai_csharpdownfiles_sample.zip - 16.1 KB)