Exportar (0) Imprimir
Expandir Tudo
Expandir Minimizar

Download do arquivo Build Smarter ASP.NET para seus aplicativos da Web

Christian Thilmany e Jim Keane

Publicado em: 2 de outubro de 2006

Este artigo discute:

  • Download dinâmico de sites ASP.NET

  • Geração imediata de links

  • Downloads reiniciáveis e manipuladores personalizados

  • Questões de segurança envolvidas em mecanismos de download personalizados

Este artigo usa as seguintes tecnologias:

  • ASP.NET

Download do código disponível em: Downloading2006_09.exe (174KB)

Nesta página

Há boas chances de que seus usuários precisem...
Forçando downloads para todos os tipos de arquivo
Download de arquivos grandes em pequenas partes
Uma solução melhor
Reiniciando downloads que falham

Há boas chances de que seus usuários precisem...

Há boas chances de que seus usuários precisem fazer o download de arquivos no site de sua organização. E, uma vez que fornecer um download é tão fácil quanto fornecer um link, você certamente não precisa ler um artigo sobre o processo, certo? Bem, graças a todo esse avanço da Web, há muitos motivos pelos quais isso talvez não seja assim tão fácil. Pode ser que você queira que o arquivo para download seja transmitido como arquivo, e não como conteúdo exibido no navegador. Pode ser que você ainda não saiba o caminho dos arquivos (ou talvez eles nem sequer estejam no disco), de modo que aqueles links HTML simples não sejam possíveis. Talvez você tenha que se preocupar com a perda de conectividade dos usuários durante downloads grandes.

Neste artigo, apresentarei algumas soluções para esses problemas de forma que seus usuários tenham uma experiência de download mais rápida e sem erros. No decorrer do artigo, abordarei links gerados dinamicamente, explicarei como ignorar comportamentos de arquivo padrão e ilustrarei downloads orientados por ASP.NET reiniciáveis usando recursos de HTTP 1.1.

O link de download básico

Ataquemos, primeiro, o problema da ausência de link. Se não sabe qual será o caminho de um arquivo, você pode, simplesmente, receber uma lista de links de um banco de dados, mais tarde. Você pode até mesmo criar a lista de links dinamicamente, enumerando os arquivos em um determinado diretório no tempo de execução. Aqui, explorarei essa segunda abordagem.

Imagine que eu tenha criado uma DataGrid no Visual Basic ® e preenchido-a com links para todos os arquivos no diretório de download, como se vê na Figura 1. Isso poderia ser feito usando o Server.MapPath dentro da página para recuperar o caminho completo até o diretório de download (neste caso, ./downloadfiles/), recuperando uma lista de todos os arquivos naquele diretório, usando DirectoryInfo.GetFiles e, em seguida, na matriz resultante dos objetos FileInfo, criando uma DataTable com colunas para cada uma das propriedades relevantes. Essa DataTable pode ser vinculada a uma DataGrid na página, por meio de links que podem ser gerados com uma definição HyperLinkColumn como:

<asp:HyperLinkColumn DataNavigateUrlField="Name"  
    DataNavigateUrlFormatString="downloadfiles/{0}" 
    DataTextField="Name" 
    HeaderText="File Name:" 
    SortExpression="Name" />

Se você clicar nos links, observará que o navegador trata cada tipo de arquivo de forma diferente, dependendo de quais aplicativos auxiliares estão registrados para cada tipo de arquivo. Por padrão, se você clicar em página .asp, página .html, .jpg, .gif ou .txt, o conteúdo será aberto no próprio navegador e não aparecerá nenhuma caixa de diálogo Salvar Como. Isso acontece porque essas extensões de arquivo são de tipos MIME conhecidos. Logo, o próprio navegador sabe como processar o arquivo ou o sistema operacional tem um aplicativo auxiliar a ser usado pelo navegador. Webcasts (.wmv, .avi e similares), PodCasts (.mp3 ou .wma), arquivos de PowerPoint ® e todos os documentos do Microsoft® Office são tipos MIME conhecidos e representam um problema, caso você não queira que eles sejam abertos embutidos por padrão.

Figura 1 Links HTML simples em uma DataGrid

Figura 1 Links HTML simples em uma DataGrid

Além disso, se você permitir downloads dessa maneira, terá um mecanismo de controle de acesso apenas genérico à sua disposição. É possível controlar o acesso a download em uma base diretório a diretório, mas controlar o acesso a arquivos ou a tipos de arquivo específicos exigiria um controle de acesso detalhado — um processo intensamente trabalhoso para webmasters e administradores de sistema. Felizmente, o ASP.NET e o .NET Framework oferecem uma variedade de soluções. Dentre elas:

  • Uso do método Response.WriteFile

  • Streaming do arquivo usando o método Response.BinaryWrite

  • Uso do método Response.TransferFile no ASP.NET 2.0

  • Uso de um filtro ISAPI

  • Programação em um controle de navegador personalizado

Forçando downloads para todos os tipos de arquivo

A mais fácil dentre as soluções empregadas que acabo de listar é o método Response.WriteFile. A sintaxe básica é muito simples; essa página ASPX completa busca um caminho de arquivo especificado na forma de um parâmetro de cadeia de caracteres de consulta e transmite o arquivo para o cliente:

<%@ Page language="VB" AutoEventWireup="false" %>
<html>
   <body>
        <%
            If Request.QueryString("FileName") Then
                Response.Clear()
                Response.WriteFile(Request.QueryString("FileName"))
                Response.End()
            End If
        %>
   </body>
</html>

Quando seu código, que está em execução em um processo de trabalho do ISS (aspnet_wp.exe no IIS 5.0 ou w3wp.exe no IIS 6.0) chama Response.Write, o processo de trabalho do ASP.NET começa a enviar dados para o processo do ISS (inetinfo.exe ou dllhost.exe). Como os dados são enviados do processo de trabalho para o processo do IIS, eles são armazenados em buffer na memória. Em muitos casos, isso não é preocupante. Entretanto, não é uma boa solução para arquivos muito grandes.

O lado bom é que, como a resposta HTTP que envia o arquivo é criada no código ASP.NET, você tem acesso total a todos os mecanismos de autenticação e autorização ASP.NET e pode, portanto, tomar decisões com base no status de autenticação, na existência de objetos Identity e Principal no tempo de execução ou em qualquer outro mecanismo que considere adequado.

Logo, é possível integrar mecanismos de segurança existentes — como os mecanismos internos de usuário e grupo do ASP.NET, suplementos do Microsoft Server (como o Gerenciador de Autorização e os grupos de função definidos), o ADAM (Modo de Aplicativo do Active Directory®) — para fornecer controle granular sobre as permissões de download.

Inicializar o download de dentro do código de seu aplicativo também permite que você substitua o comportamento padrão para tipos MIME conhecidos. Para fazer isso, é necessário alterar o link exibido. Eis o código para construir um hiperlink que levará de volta à página ASPX:

<!-- in the DataGrid definition in FileFetch.aspx -- >
<asp:HyperLinkColumn DataNavigateUrlField="Name"          
    DataNavigateUrlFormatString="FileFetch.aspx?FileName={0}" 
    DataTextField="Name" 
    HeaderText="File Name:" 
    SortExpression="Name" />

Em seguida, será necessário verificar a Seqüência de Consulta quando a página for solicitada, para ver se o pedido é um postback que inclui um argumento filename a ser enviado ao navegador do cliente (veja a Figura 2). Agora, graças ao cabeçalho de resposta Content-Disposition, quando se clica em um dos links na grade, a caixa de diálogo Salvar é exibida independente do tipo MIME (veja a Figura 3). Observe, ainda, que restringi os arquivos que podem ter o download efetuado com base no resultado da chamada de um método denominado IsSafeFileName. Para obter mais informações sobre por que fiz isso e o que esse método faz, consulte a barra lateral "Acesso involuntário a arquivos".

Figura 3 Forçando uma caixa de diálogo de download de arquivo

Figura 3

Forçando uma caixa de diálogo de download de arquivo

Uma métrica importante a ser considerada ao usar essa técnica é o tamanho de download do arquivo. Você deve limitar o tamanho do arquivo ou irá expor seu site a ataques de negação de serviço. Tentativas de download de arquivos maiores do que os recursos permitem irão gerar um erro no tempo de execução declarando que a página não pode ser exibida ou, então, exibirá um erro como este:

Server Application Unavailable

The Web application you are 
attempting to access on this Web 
server is currently unavailable. 
Please hit the "Refresh" button in your Web 
browser to retry your request.

Administrator Note: An error message detailing 
the cause of this specific request failure can be 
found in the system event log of the Web server. 
Please review this log entry to discover what 
caused this error to occur. 

O tamanho de arquivo máximo permitido para download é um fator da configuração de hardware e da instrução runtime do servidor. Para lidar com esse problema, consulte o artigo da Base de Dados de Conhecimento "FIX: o download de arquivos grandes provoca uma grande perda de memória e faz com que o processo Aspnet_wp.exe recicle", em support.microsoft.com/kb/823409.

Esse método pode ser sintomático durante o download de arquivos grandes, como vídeos, particularmente em servidores Web que executam Windows 2000 e ISS 5.0 (ou Windows Server 2003 com IIS 6.0 executando no modo de compatibilidade). O problema será exacerbado nos servidores Web que estiverem com memória minimamente configurada, uma vez que o arquivo deve ser primeiro carregado na memória do servidor para que possa sofrer download para o cliente.

Evidência empírica gerada em minha máquina de teste, um servidor executando IIS 5.0 com 2GB de RAM, indica falha no download quando o tamanho de arquivo se aproxima de 200MB. Em um ambiente de produção, quanto mais downloads simultâneos os usuários fizerem, mais restrições de memória do servidor resultarão em falhas de download. A solução para esse problema requer um pouco mais de linhas simples de código.

Download de arquivos grandes em pequenas partes

O problema do tamanho de arquivo com o código de amostra anterior provém da chamada única de Response.WriteFile, o que provoca o armazenamento em buffer de todo o arquivo de origem na memória. Uma abordagem melhor, no caso de arquivo grande, é lê-lo e enviá-lo ao cliente em blocos menores e mais gerenciáveis; um exemplo disso é apresentado na Figura 4. Essa versão do manipulador de eventos Page_Load usa um loop while para ler 10.000 bytes do arquivo por vez e, depois, envia os blocos para o navegador. Portanto, nenhuma porção significativa do arquivo é mantida na memória em tempo de execução. O tamanho do bloco está definido atualmente como uma constante, mas ele também poderia ser modificado por programação ou, até mesmo, movido para um arquivo de configuração para que pudesse ser alterado de modo a atender às restrições de servidor e às necessidades de desempenho. Testei esse código com arquivos de até 1.6GB e os downloads foram rápidos e não resultaram em consumo significativo da memória do servidor.

O próprio IIS não aceita downloads de arquivos de tamanho maior que 2GB. Se precisar de downloads maiores, você terá que usar FTP, um controle de terceiros, o Microsoft Background Intelligent Transfer Service (BITS) ou uma solução personalizada, como fazer o streaming dos dados por meio de soquetes para um controle personalizado hospedado no navegador.

Uma solução melhor

A semelhança das necessidades de download de arquivos e o tamanho sempre crescente dos arquivos em geral fizeram com que a equipe de desenvolvimento do ASP.NET introduzisse nele um método específico para o download de arquivos sem buffer na memória antes de enviá-lo ao navegador. Trata-se do método Response.TransmitFile, disponível no ASP.NET 2.0.

O TransmitFile pode ser usado da mesma maneira que o WriteFile, mas produz, tipicamente, melhores características de desempenho. O TransmitFile também vem competir com outras funcionalidades. Observe o código na Figura 5, que usa alguns recursos adicionais do TransmitFile recém-adicionado para evitar os problemas de uso de memória já mencionados.

Consegui adicionar alguma segurança e tolerância a falhas com umas poucas linhas a mais de código. Primeiro, acrescentei um bit de segurança e restrição lógica, usando a extensão do arquivo solicitado para determinar o tipo MIME e especificar o tipo MIME requisitado em um Cabeçalho HTTP, definindo a propriedade "ContentType" do objeto Response:

Response.ContentType = "application/x-zip-compressed"

Isso me permitiu limitar os downloads a somente certos tipos de conteúdo e mapear diferentes extensões de arquivo para um único tipo de conteúdo. Observe, ainda, a instrução que adiciona um cabeçalho Content-Disposition. Essa instrução permite-me especificar o nome do arquivo a sofrer download, separado do nome de arquivo original no disco rígido do servidor.

Neste código, criei um novo nome de arquivo acrescentando um prefixo ao nome original. Embora o prefixo aqui seja estático, eu poderia criar dinamicamente um prefixo para que o nome do arquivo baixado nunca entre em conflito com um nome de arquivo já existente no disco rígido do usuário.

Mas e se, no meio da extração de um arquivo grande, o meu download falhar? Embora o código até o momento já tenha percorrido um longo caminho desde o link de download simples, eu, felizmente, ainda posso manipular um download que falhou e reiniciar o download de um arquivo que já foi parcialmente movido do servidor para o cliente. Todas as soluções que examinei até agora exigiriam que o usuário reiniciasse o download do começo outra vez, na eventualidade de uma falha.

Reiniciando downloads que falham

Para tratar da questão de reinicialização de um download que falhou, voltemos à abordagem de dividir manualmente um arquivo para transmissão. Embora não seja tão simples quanto o código que usa o método TransmitFile, há uma vantagem em programar manualmente o código a ser lido e enviar o arquivo em blocos. Em qualquer momento, a instrução runtime conterá o número de bytes já enviados ao cliente e, subtraindo-o do tamanho total do arquivo, você obterá o número de bytes que restam a serem transmitidos para a conclusão do arquivo.

Se você olhar novamente o código, observará que o loop read/send verifica, enquanto condição de loop, o resultado de Response.IsClientConnected. Esse teste assegura a suspensão da transmissão, se o cliente não estiver mais conectado. Na primeira iteração de loop em que o teste é falso (o navegador da Web que iniciou o download do arquivo não está mais conectado), o servidor interrompe o envio de dados e os bytes que restam para completar o arquivo podem ser gravados. Mais do que isso, o arquivo parcial recebido pelo cliente pode ser salvo, na eventualidade de o usuário desejar completar, mais tarde, o download que falhou.

O resto da solução de download reiniciável vem por meio de alguns recursos pouco conhecidos no protocolo HTTP 1.1. Normalmente, a natureza de ausência de monitoração de estado do HTTP é a ruína existencial dos desenvolvedores Web, mas, neste caso, a especificação de HTTP é de grande ajuda. Particularmente, existem dois elementos de cabeçalho no HTTP 1.1 relativos à tarefa em pauta: Accept-Ranges e Etag.

O elemento de cabeçalho Accept-Ranges diz ao cliente (neste caso, o navegador da Web), muito simplesmente, que o processo aceita downloads reiniciáveis. O elemento Entity Tag, ou Etag, especifica um identificador exclusivo para a sessão. Assim, os Cabeçalhos HTTP que o aplicativo ASP.NET poderia enviar ao navegador para iniciar um download reiniciável poderiam ter esta aparência:

HTTP/1.1 200 OK
Connection: close
Date: Mon, 22 May 2006 11:09:13 GMT
Accept-Ranges: bytes
Last-Modified: Mon, 22 May 2006 08:09:13 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 39551221

Por causa do ETag e do Accept-Headers, o navegador sabe que o servidor da Web dará suporte a downloads reiniciáveis.

Se o download falhar, quando o arquivo for solicitado novamente, o Internet Explorer enviará o ETag, o nome de arquivo e o intervalo de valores indicando quanto do arquivo já foi baixado com êxito antes da interrupção, de modo que o servidor da Web (IIS) possa tentar reiniciar o download. Essa segunda solicitação poderia se parecer com isto:

GET http://192.168.0.1/download.zip HTTP/1.0
Range: bytes=933714-
Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT
If-Range: "58afcc3dae87d52:3173"

Observe que o elemento If-Range contém o valor de ETag original, que o servidor pode usar para identificar o arquivo a ser reenviado. Você perceberá também que o elemento Unless-Modified-Since contém a data e a hora de início do download original. O servidor usará essa informação para determinar se o arquivo foi modificado desde o início do download original. Em caso positivo, o servidor reiniciará o download desde o começo.

O elemento Range, que também está no cabeçalho, informa ao servidor quantos bites são necessários para completar o arquivo, o que o servidor pode usar para determinar em que ponto do arquivo parcialmente enviado ele deve recomeçar.

Outros navegadores usam esses cabeçalhos de maneira ligeiramente diversa. Outros cabeçalhos HTTP que um cliente pode enviar para identificar o arquivo com exclusividade são: If-Match, If-Unmodified-Since e Unless-Modified-Since. Observe que o HTTP 1.1 não é específico sobre quais cabeçalhos um cliente deve obrigatoriamente aceitar. No entanto, é possível que alguns navegadores da Web não dêem suporte a alguns desses cabeçalhos HTTP ou podem usar um cabeçalho diferente daqueles esperados pelo Internet Explorer®.

Por padrão, o IIS incluirá um conjunto de cabeçalhos como este:

HTTP/1.1 206 Partial Content
Content-Range: bytes 933714-39551221/39551222
Accept-Ranges: bytes
Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 2021408

Esse conjunto de cabeçalhos inclui um código de resposta diferente daquele do pedido original. A resposta originária incluía um código de 200, ao passo que esse pedido usa um código de resposta de 206, Reiniciar Download, que diz ao cliente que os dados que seguirão não são um arquivo completo, mas a continuação de um download iniciado anteriormente, cujo nome de arquivo está identificado pelo ETag.

Embora alguns navegadores da Web se baseiem no próprio nome de arquivo, o Internet Explorer solicita muito especificamente o cabeçalho ETag. Se o cabeçalho ETag não estiver presente na resposta de download inicial ou na continuidade do download, o Internet Explorer não tentará reiniciá-lo, mas simplesmente dará início a um novo download.

Para que o aplicativo de download ASP.NET implemente um recurso de download reiniciável, é necessário interceptar o pedido (de continuidade do download) do navegador e usar os cabeçalhos HTTP no pedido para formular uma resposta apropriada no código ASP.NET. Para tanto, você deve apanhar a solicitação um pouco mais cedo na seqüência normal de processamento.

Por sorte, o .NET Framework está aqui para ajudar. Esse é um ótimo exemplo de premissa de design fundamental do .NET — fornecer uma biblioteca de objetos bem fabricada e funcional para uma grande parte do trabalho de direcionamento padrão solicitada aos desenvolvedores.

Neste caso, é possível aproveitar a interface IHttpHandler fornecida pelo namespace System.Web no .NET Framework para criar seu próprio manipulador HTTP personalizado. Criando sua própria classe que implementa o IHttpHandler, você poderá interceptar solicitações da Web de um tipo de arquivo específico e respondê-las em seu próprio código, em vez de simplesmente permitir ao IIS responder com seus comportamentos padrão.

O download de código deste artigo contém uma implementação funcional de um manipulador HTTP que dá suporte a downloads reiniciáveis. Embora haja uma boa quantidade de código para esse recurso e sua implementação requeira um pouco de compreensão da mecânica do HTTP, o .NET Framework, contudo, torna a implementação relativamente simples. Essa solução oferece a capacidade de fazer o download de arquivos muito grandes, com a vantagem de o navegador ser liberado depois de iniciado o download. Todavia, há certas considerações de infra-estrutura que estarão fora de seu controle.

Por exemplo, muitas companhias e provedores de serviço de Internet mantêm seus próprios mecanismos de cache. Servidores de cache da Web danificados ou malconfigurados podem causar falha em downloads grandes, devido a danos no arquivo ou término prematuro da sessão, especialmente se o arquivo tiver tamanho superior a 255MB.

Se forem necessários downloads superiores a 255MB ou outras funções personalizadas, convém considerar o uso de gerenciadores de download personalizados ou de terceiros. Você pode, por exemplo, criar um controle de navegador personalizado ou uma função auxiliar ao navegador para gerenciar os downloads, passá-los para o BITS ou, até mesmo, passar o arquivo para um cliente FTP no código personalizado. As opções são intermináveis e podem ser adaptadas para suas necessidades específicas.

De downloads de arquivos grandes em duas linhas de código até downloads segmentados e reiniciáveis com segurança personalizada, o .NET Framework e o ASP.NET fornecem uma ampla variedade de opções para a criação da experiência de download mais adequada aos usuários finais do seu site.

Joe Stagner entrou para a Microsoft em 2001 como Técnico Especialista e hoje é Gerente de Programas para a Comunidade de Desenvolvedores no grupo Produtos para Ferramentas e Plataforma. Seus 30 anos de experiência em desenvolvimento propiciaram-lhe a oportunidade de criar software aplicativos comerciais em uma ampla diversidade de plataformas técnicas.

Mostrar:
© 2014 Microsoft