Skip to content

Tag Archives: python

Django, download e o problema no Internet Explorer 7

09-May-08

Há pouco tempo atrás eu postei aqui no blog como forçar o download de arquivos com Django.

Na época eu havia testado o sistema em todos os navegadores que estavam ao meu alcance (Firefox, Iceweasel, Opera, Epiphany, IE6 e IE7) porém mesmo assim não precisou de muito tempo em ambiente de produção para me contactarem dizendo que o sistema não estava funcionando no IE7 (ué, como assim!? mas eu tinha testado isso!).

Testei novamente e então descobri que na verdade ele funcionava e não funcionava. Hm, ok… mas como assim!? Na verdade ele funcionava perfeitamente no IE7 do Windows 2003 Server, mas não funcionava em nenhum outro e eu dei o azar (ou sorte) de testar justamente nele… no Windows 2003 Server. Nos Windows com problemas (pleonasmo?) o erro que ocorria era o seguinte:

Internet Explorer was not able to open this Internet site.
The requested site is either unavailable or cannot be found. Please try again later.

Ok, um erro muito explicativo… Então vamos ao Google! Depois de muito procurar no Google sem encontrar nenhuma solução, um colega de empresa decidiu comparar os headers de uma aplicação escrita em .NET e que funcionava em qualquer IE, com os headers gerados pela minha aplicação. Chegamos a conclusão de que o problema estava no campo Vary, que aparecia nos headers do Django mas não existia no header da aplicação .NET.

A primeira idéia foi remover o campo manualmente, antes de retornar o response da página:


from django.http import HttpResponse, Http404
from os import path
import mimetypes

def index(request, file=''):
    abspath = path.join('/your/absolute/path', file)
    if not (file and path.exists(abspath)):
        raise Http404()
    mimetype, encoding = mimetypes.guess_type(abspath)
    if mimetype is None:
        mimetype = 'application/force-download'
    response = HttpResponse(open(abspath, 'r').read())
    response['Content-Disposition'] = 'attachment; filename=%s' % file
    response['Content-Type'] = mimetype
    response['Content-Length'] = str(path.getsize(abspath))
    del response['Vary']
    return response

* As linhas em negritos são as linhas que foram alteradas na solução inicial.

Mas não funcionou… O campo Vary continuava aparecendo e com o mesmo valor de antes. Lendo um pouco de documentação, descobrimos que essa diretiva não pode ser alterada diretamente no header da página e também qual era a forma correta de remover esse campo:


from django.views.decorators.cache import cache_control
from django.http import HttpResponse, Http404
from os import path
import mimetypes

@cache_control(private=True)
def index(request, file=''):
    abspath = path.join('/your/absolute/path', file)
    if not (file and path.exists(abspath)):
        raise Http404()
    mimetype, encoding = mimetypes.guess_type(abspath)
    if mimetype is None:
        mimetype = 'application/force-download'
    response = HttpResponse(open(abspath, 'r').read())
    response['Content-Disposition'] = 'attachment; filename=%s' % file
    response['Content-Type'] = mimetype
    response['Content-Length'] = str(path.getsize(abspath))
    return response

* As linhas em negritos são as linhas que foram alteradas na solução inicial.

Ok, agora sim! Tudo funcionando direitinho e em todos os (pseudo-)navegadores!

Como eu não achei nada na internet que ligasse esse problema aos campos Vary do header, acredito que esse post possa ajudar alguém mais que esteja passando pelo menos problema! :)

UPDATE: Em alguns casos a conexão terminava antes de ter baixado todo o arquivo. Esse é um problema do servidor, e para corrigi-lo basta definir a diretiva EnableSendfile do Apache como On. Com essa configuração, também houve uma melhora significativa na taxa de download do servidor (pelo menos no meu caso).

Forçando o download de arquivos com Django

08-Apr-08

Primeiro post pelo Planeta GNU/Linux Brasil. :)

Essa semana precisei criar uma aplicação web que disponibilizasse alguns arquivos para download e então decidi fazer isso utilizando Python e Django.

Uma parte da minha aplicação consistia em forçar o browser a abrir uma janela de download ao invés de exibir o arquivo (como acontece no caso de imagens ou arquivos texto). Eu já havia feito isso algumas vezes em PHP, mas fui pesquisar para ver se o Django não possuia alguma função pronta que me facilitasse essa tarefa, algo como:


return HttpResponseFile(file)

Passei algum tempo procurando por alguma solução mais inteligente do que alterar o header na mão, mas não consegui encontrar nada. Fui então atrás da documentação oficial, para descobrir como eu podia aplicar a solução que eu já utilizava em PHP para a realidade do Django. Encontrei aqui a “receita de bolo” de como alterar o header da requisição e fazer com que o browser agisse da forma como eu queria.

Portanto segue abaixo, a parte principal da minha implementação:

urls.py

Esse arquivo tem uma única função: extrair da url o nome do arquivo a ser baixado e passa-lo como parametro para a view.


urlpatterns = patterns('',
    (r'(?P<file>(^.*$)', 'myproject.apps.download.views.index'),
)

views.py

Aqui é onde a mágica ocorre… na verdade não é nada muito mágico, mas enfim. :)

Essa view apenas verifica se o arquivo existe e o envia para download, caso o arquivo não exista, retorna a página 404. Nessa função você pode aplicar várias outras validações, como verificar se o usuário está logado ou se ele relamente tem acesso a esse arquivo, etc. porém nada disso era necessário no meu caso.


from django.http import HttpResponse, Http404
from os import path
import mimetypes

def index(request, file=''):
    abspath = path.join('/your/absolute/path', file)
    if not (file and path.exists(abspath)):
        raise Http404()
    mimetype, encoding = mimetypes.guess_type(abspath)
    if mimetype is None:
        mimetype = 'application/force-download'
    response = HttpResponse(open(abspath, 'r').read())
    response['Content-Type'] = mimetype
    response['Pragma'] = 'public'
    response['Expires'] = '0'
    response['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0'
    response['Content-Disposition'] = 'attachment; filename=%s' % file
    response['Content-Transfer-Encoding'] = 'binary'
    response['Content-Length'] = str(path.getsize(abspath))
    return response

Espero que seja de alguma utilidade… e se alguém tiver uma solução melhor! Por favor, poste nos comentários.

Não faz muito tempo que eu mexo com Django, mas estou gostando da brincadeira!
Acredito que o número de posts sobre Python desse blog tende a crescer consideravelmente… e isso é bom!

UPDATE: Depois de algum tempo com o sistema em produção, foi constatado que a solução acima não funciona em algumas versão do Internet Explorer 7. A solução do problema também foi documentada e está disponível aqui.

Modulo python para o modem D-Link 500G

11-Mar-08

No meu último post eu falei sobre como acessar via telnet a interface modo texto (aka CLI) do modem D-Link 500G. Para quem gosta de trabalhar no terminal só isso já é uma maravilha, porém essa descoberta tem algumas outras vantagens…

E se nós pudéssemos controlar/manipular o modem através de scripts? Isso sim seria legal… Mas como criar um shell script para interagir com telnet é um tanto quanto chato, eu criei um módulo python para fazer isso. Então seguindo os exemplos do posts passado, vamos ver como fazer a mesma coisa porém dessa vez através do módulo Python:

Imprimir o help

Observe que foram criadas constantes para os valores padrão de configuração do modem, portanto se você usa o aparelho da maneira como ele lhe foi entregue, provavelmente não precisará alterar nada no códgo abaixo.


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from dlink500g import *

try:
    modem = DLink500G(DEFAULT_ADDRESS, DEFAULT_PORT)
    modem.connect(DEFAULT_USERNAME, DEFAULT_PASSWORD)
    print modem.command('help')
    modem.command('quit')
except Exception, ex:
    print 'Error:', str(ex)

Reiniciar o modem


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from dlink500g import *

try:
    modem = DLink500G(DEFAULT_ADDRESS, DEFAULT_PORT)
    modem.connect(DEFAULT_USERNAME, DEFAULT_PASSWORD)
    modem.command('reboot')
except Exception, ex:
    print 'Error:', str(ex)

Código fonte

Abaixo está o conteúdo da primeira versão deste módulo, que provavelmente sofrerá alterações e/ou bugfixes, portanto para se você deseja obter uma versão mais atualizada acesse o código fonte disponível no meu repositório svn do Google Code.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This module provides a python interface to D-Link 500G CLI.
#
# written by Arthur Furlan <arthur.furlan@gmail.com>

import re, telnetlib

# default configuration
DEFAULT_ADDRESS  = '10.0.0.1'
DEFAULT_PORT = 23
DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = 'admin'

class DLink500G:
    def __init__(self, address, port):
        self.address = address
        self.port = port
        self.connected = False

    def __del__(self):
        if self.connected:
            self.command('quit')
        self.telnet.close()

    def connect(self, username, password):
        self.telnet = telnetlib.Telnet(self.address, self.port)
        self.telnet.read_until('login: ')
        self.telnet.write("%s\n" % username)
        self.telnet.read_until('password: ')
        self.telnet.write("%s\n" % password)
        response = self.telnet.expect(['\$'], 5)
        if response[0] == -1:
            raise Exception('Login failed.')
        self.connected = True

    def command(self, cmd):
        if not self.connected:
            raise Exception('Not logged in.')
        self.telnet.write("%s\n" % cmd)
        self.connected = not (cmd == 'quit')
        if self.connected:
            response = self.telnet.read_until('$').replace('\r', '').split('\n')
            if re.match('^Error: ', response[1]):
                raise Exception(re.sub('^Error: ', '', response[1]))
            return '\n'.join(response[1:len(response)-1])

Sinta-se a vontade para enviar-me crítricas e/ou sugestões. :)

UPDATE1: Seguindo o “conselho” do Eduardo Willians, alterei a visibilidade dos atributos para público.

UPDATE2: Aproveitei o embalo para criar um projeto no Google Code, cuja a intenção é criar um pacote com módulos para vários dispositivos da D-Link que seguem a mesma linha. Lá você poderá encontrar as versões mais recentes desse módulo.

UPDATE3: Notícia divulgada também no br-linux.org

Criando um servidor TCP concorrente em Python

05-Aug-07

No semestre passado da faculdade cursei a disciplina de Redes II. Nessa matéria uma das formas de avaliação é sempre composta de um trabalho prático juntamente com os devidos relatórios. No meu semestre o trabalho consituia em desenvolver um servidor TCP concorrente que atenda à N conexões, podendo ser desenvolvido em qualquer linguagem. Eu fiz o trabalho em dupla com o Thiago Mendes e nós optamos por fazer o nosso em Python. Depois da explicação toda, segue abaixo a nossa implementação:

Servidor

Esse arquivo implementa a classe Server, cuja finalidade é ouvir a porta definida em PORT e criar processos filhos (aka fork) para as requisições recebidas. Depois de criados os sub-processos, trabalha como uma espécie “chat” entre cliente e servidor transmitindo mensagens de um para o outro.

Obs.: Note que como esse programa era para fins acadêmicos, ele está cheio de comentários explicando o comportamento do mesmo.


#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
from socket import *
from settings import *

class Server:
    __addr = (HOST, PORT) # endereço de funcionamento do servidor
    __sock = None # instancia do socket para o processo pai
    __conn = None # instancia do socket para os processos filhos
    __client = 0 # numero do cliente que está sendo atendido

    def __init__(self):
        try:
            print ' * Inicializando o servidor TCP'
            print ' * Instancinado o socket do protocolo TCP'
            self.__sock = socket(AF_INET, SOCK_STREAM) # instancia o socket
        except:
            print 'ERRO: Não foi possível instanciar o socket'
            sys.exit(1)

    def __del__(self):
        self.__sock.close() # fecha o socket utilizado pelo processo pai
        del(self.__sock)

    def __connect(self):
        try:
            print ' * Alocando porta local ' + str(self.__addr[1])
            self.__sock.bind(self.__addr) # reserva a porta local
            self.__sock.listen(5) # define o tamanho da fila de espera
        except:
            print 'ERRO: Não foi possível alocar a porta local', self.__addr[1]
            sys.exit(1)

    def run(self):
        self.__connect() # instancia o socket e aloca uma porta local
        while 1:
            try: # isso evita que seja exibida a mensagem de erro quando terminar o servidor com CTRL+C
                self.__conn, self.__addr = self.__sock.accept() # aceita a requisição do cliente
            except:
                sys.exit(1)
            self.__client += 1 # incrementa o valor para o novo cliente que está sendo atendido

            print ' * Requisição recebida de', self.__addr[0] + ':' + str(self.__addr[1])
            print ' * Criando processo filho para antender cliente', self.__client

            if os.fork(): # cria um processo filho para atender a nova requisição
                self.__conn.close() # fecha a conexão com o processo filho
            else:
                data = None
                while data != "quit": # executa enquanto o cliente estiver executando
                    data = self.__conn.recv(BUFSIZ) # recebe a mensagemd o cliente
                    self.__conn.send(data) # envia a mensagem de volta, confirmando o recebimento
                    print " * Recebida a mensagem '" + data + "' do cliente", self.__client
                self.__conn.close() # fecha a conexão do processo filho
                print ' * Encerrando conexão com o cliente', self.__client
                sys.exit(1)

if __name__ == "__main__":
    server = Server()
    server.run()

Cliente

Esse arquivo implementa a classe Client, cuja finalidade é estabelecer conexões com o servidor definido em HOST e PORT. Depois de estabelecido o canal de comunicação, o programa executa trabalha como uma espécie “chat” trocando mensagens com o servidor, o programa é encerrado quando a mensagem “quit” é enviada pelo cliente.

Obs.: Assim como o servidor, este programa está cheio de comentários explicando tudo o que acontece.


#!/usr/bin/env python
# -*- coding: utf-8 -*-
from socket import *
from settings import *

class Client:
    __addr = (HOST, PORT) # endereço de funcionamento do servidor
    __sock = None # instancia do socket para o processo pai

    def __init__(self):
        try:
            print ' * Inicializando o cliente TCP'
            print ' * Instancinado o socket do protocolo TCP'
            self.__sock = socket(AF_INET, SOCK_STREAM) # instancia o socket
        except:
            print 'ERRO: Não foi possível instanciar o socket'
            exit(1)

    def __def__(self):
        self.__sock.close() # fecha o socket utilizado pelo processo
        del(self.__sock)

    def __connect(self):
        try:
            print ' * Conectando com o servidor em', self.__addr[0] + ':' + str(self.__addr[1])
            self.__sock.connect(self.__addr) # cria a conexao
        except:
            print 'ERRO: Não foi possível conectar com o servidor'
            exit(1)

    def run(self):
        self.__connect() # instancia o socket e conecta com o servidor
        data = None
        while data != "quit": # executa até quando o usuário desejar sair
            print ' >>>',
            data = str(raw_input()) # recebe a mensagem do cliente
            self.__sock.send(data) # envia a mensagem para o servidor
            data = self.__sock.recv(BUFSIZ) # recebe confirmação do envio
        print ' * Encerrando conexão com o servidor'

if __name__ == "__main__":
    client = Client()
    client.run()

Configuração

Define as configurações da aplicação. Cliente e servidor irão rodar em em localhost e o servidor atenderá requisições na porta 5002.


HOST = 'localhost'
PORT = 5002
BUFSIZ = 1024

Log do sistema operacional no servidor

Log do sistema operacional com o servidor em funcionamento. As primeiras versões do trabalho deixavam alguns processos filhos abertos mesmo depois de finalizado o servidor, o que foi corrigido e explicitado abaixo.


# antes de iniciar todos os processos
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan@isengard:

# iniciado o servidor
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan   9657  0.5  0.6   6048  3360 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan@isengard:

# iniciado o cliente 1
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan   9657  0.2  0.6   6048  3384 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan   9663  0.0  0.3   6048  1548 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan@isengard:

# iniciado o cliente 2
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan   9657  0.1  0.6   6048  3384 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan   9663  0.0  0.3   6048  1548 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan   9671  0.0  0.3   6048  1548 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan@isengard:

# encerrado o cliente 1
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan   9657  0.1  0.6   6048  3384 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan   9671  0.0  0.3   6048  1548 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan@isengard:

# encerrado o cliente 2
afurlan@isengard:~/redes2$ ps ux | grep python | grep servr
afurlan   9657  0.0  0.6   6048  3384 pts/0    S+   12:02   0:00 /usr/bin/python ./server
afurlan@isengard:

# encerrado o servidor
afurlan@isengard:~/redes2$ ps ux | grep python | grep server
afurlan@isengard:~/redes2$

O Danilo Cesar cursou essa matéria comigo e também desenvolveu uma versão desse servidor, portanto encham o saco para ele postar a sua porque que ficou bem bacana :)

Clique aqui para baixar um tarball contendo todos os arquivos acima (com excessão do log).