Amazon S3 es un gran servicio para alojar los archivos de una página web,
ofrece alta disponibilidad, fiabilidad, y seguridad (configurado correctamente).
Pero en páginas con demasiado tráfico, con una gran cantidad de peticiones,
y/o ancho de banda, como por ejemplo páginas que alojen gran cantidad de
imágenes, puede hacer el coste de S3 prohibitivo.
En estos casos la alternativa es usar un CDN, o gestionar tu propio
sistema de distribución. Un CDN puede resultar algo más barato dependiendo
del tráfico, pero no significativamente. Gestionar tu propio servidor aumenta
la complejidad de la aplicación, los posibles puntos de fallo, es menos
escalable, y más difícil de administrar.
Existe una solución intermedia, que consiste en seguir almacenando los
archivos en S3, y usar Varnish como proxy cache. De esta manera cuando
una petición de un archivo llega, Varnish lo obtiene de S3, se lo envía
al cliente, y guarda una copia en caché. Sucesivas peticiones de ese archivo
se sirven directamente desde cache.
Con esta configuración no es necesario modificar el backend de almacenamiento
ya que se sigue usando S3, es altamente escalable, facil de administrar, y
permite seguir usando S3 como fallback.
Configuración de Varnish
Este artículo no es un tutorial de Varnish, si eso es lo que estas buscando,
hay muchos disponibles en la red, pero te recomiendo que empieces por los
enlaces al final del articulo.
Archivo de configuración para Varnish con S3 default.vcl:
/* Amazon S3 Backend */
backend s3 {
.host = "nombrebucket.s3.amazonaws.com";
.port = "80";
}
/* vcl_recv es llamado cada vez que una petición es recibida */
sub vcl_recv {
/* Solo servimos peticiones GET y HEAD */
if (req.request != "GET" && req.request != "HEAD") {
return (pass);
}
/* Solo servimos archivos, asi que ni Cookies ni Autorizaciones */
if (req.http.Authorization || req.http.Cookie) {
return (pass);
}
set req.grace = 120s;
set req.http.X-Forwarded-For = client.ip;
/* Establecer backend para la peticion */
set req.backend = s3;
set req.http.host = "nombrebucket.s3.amazonaws.com";
/* Eliminar de la cabecera todos los campos que pueden afectar la cache */
unset req.http.Cache-Control;
unset req.http.Pragma;
unset req.http.Expires;
/* Si el archivo es una imagen desactivamos compresion */
if (req.url ~ "\.(jpeg|jpg|png|ico|svg|gif)$") {
unset req.http.Accept-Encoding;
return (lookup);
}
else if (req.url ~ "\.(css|js|txt)$") {
return (lookup);
}
/* Demas tipos no soportados */
return (pass);
}
/* Llamado cuando hay un hit en cache */
sub vcl_hit {
return (deliver);
}
/* Llamado cuando hay un miss en cache */
sub vcl_miss {
return (fetch);
}
/* Llamado cuando un documento se ha descargado exitosamente del backend */
sub vcl_fetch {
/* Se eliminan cookies antes de que el objeto sea introducido en cache */
unset beresp.http.Set-Cookie;
/* Si es una imagen se desactiva compresion (Solo precaucion) */
if (req.url ~ "\.(jpg|jpeg|png|ico|svg|gif)$") {
set beresp.do_gzip = false;
}
/* Establecer fecha de caducidad para los datos en cache */
set beresp.ttl = 1w; /* Una semana */
set beresp.grace = 120s;
/* Forzar cacheado por el cliente */
set beresp.http.Expires = beresp.ttl;
set beresp.http.Cache-Control = "max-age=604800"; /* 7 dias */
/* Ocultar información añadida por amazon antes de almacenar */
unset beresp.http.Server;
unset beresp.http.x-amz-id-2;
unset beresp.http.x-amz-request-id;
unset beresp.http.x-amz-version-id;
unset beresp.http.ETag;
return (deliver);
}
sub vcl_deliver {
/* Eliminar informacion superflua antes de enviar respuesta */
unset resp.http.Via;
unset resp.http.X-Varnish;
/* DEBUG: Util para comprobar el correcto funcionamiento de la cache */
/* unset resp.http.Age; */
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
return (deliver);
}
Descargar
El archivo de configuración del demonio es /etc/default/varnish, y en el
hay que modificar el puerto de escucha y especificar como queremos que
Varnish almacene la cache, esto dependerá de la cantidad de memoria
disponible, el tamaño del bucket, y la latencia del disco duro. En mi
caso el servidor es un VPS con 512MB de RAM y una SSD de 20GB alojado en
Digital Ocean, y como no tiene suficiente
RAM, uso un archivo para la cache:
DAEMON_OPTS="-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-S /etc/varnish/secret \
-s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,4G"
Si tienes disponible suficiente memoria para almacenar la cache en RAM, podrías
usar la configuración:
DAEMON_OPTS="-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-S /etc/varnish/secret \
-s malloc,4G"
Una vez configurado y reiniciado, puedes comprobar que esta funcionando
correctamente con curl.
Al descargar cualquier documento por primera vez debería contener
X-Cache: MISS, sucesivas peticiones del mismo documento contendrán
X-Cache: HIT si la cache esta funcionando.
$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 0
Connection: keep-alive
X-Cache: MISS
$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 8
Connection: keep-alive
X-Cache: HIT
Cuando compruebes que funciona ya puedes comentar la sección que añade el campo
X-Cache.
Configurar Django
Si usas Django, una vez Varnish este funcionando hay que indicar a Storage,
como generar la dirección para los archivos que antes se accedían a traves de
s3.amazoaws.com. El parámetro AWS_S3_CUSTOM_DOMAIN permite apuntar al
dominio que estés usando para tu servidor Varnish.
AWS_S3_CUSTOM_DOMAIN = 'servidorvarnish.tudominio.com'
Si no estás usando el puerto 80 puedes espeficicarlo con:
AWS_S3_CUSTOM_DOMAIN = 'www.tudominio.com:8080'
Click to read and post comments
Los programas que recopilan información de una página web en busca de
información se llaman crawler, spider, o arañas en castellano.
Es una tarea tan común, que existen multitud de bibliotecas y frameworks con
este propósito, en algunos casos simplificando la programación hasta el punto
en el que se puede programar un crawler sencillo con menos de 100 lineas de
código. Y eso es justamente lo que voy a hacer en este post.
Imaginemos que quiero crear una pequeña base de datos para organizar que
libros he leído, y cuales me faltan por leer, pero no quiero tener que
introducir manualmente la información de cada libro, así que voy a obtener
toda la informacón de una página web, por ejemplo
epublibre.
Para programar la araña me he decantado por Python, y un framework llamado
Scrapy del que he leido mucho últimamente.
Instalación
La instalación de un paquete en python suele ser muy sencilla, pero en este
caso he tenido algún problema con las dependencias, y he necesitado instalar
algunos paquetes en el sistema:
$ apt-get install libxml2-dev libxslt-dev libffi-dev
Tras esto se instala scrapy con pip:
Uso
El primer paso es iniciar un nuevo proyecto:
$ scrapy startproject epublibre_crawler
Esto crea un directorio de proyecto con la siguiente estructura:
epublibre_crawler/
├── epublibre_crawler
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
Una vez creado el proyecto, tenemos que definir los items que queremos extraer,
o mejor dicho la clase donde se almacenaran los datos extraídos por
scrapy. En mi caso el titulo del libro, y su autor es suficiente, así que
añadimos al archivo items.py la subclase de Item:
from scrapy.item import Item, Field
class BookItem(Item):
title = Field()
author = Field()
El siguiente paso es describir usando expresiones XPath, como se puede extraer
la información del titulo y autor, de manera que Scrapy pueda diferenciarla
del resto de código html de la página de cada libro.
Si miramos el código html de la página de un libro, el título está contenido
en un div de la forma:
<div class="det_titulo" id="titulo_libro" style="display:inline-block;">
Omega
</div>
Dado que el titulo tiene un identificador único "titulo_libro", es fácil de
extraer:
xpath("//div[@id='titulo_libro']/text()[normalize-space()]")
para el nombre del autor, tenemos:
<div class="negrita aut_sec" style="display:inline-block;">
<a href="http://www.epublibre.org/autor/index/425">Jack McDevitt</a>
</div>
este es el único div de la página que usa la clase aut_sec, así que podemos
extraer el nombre con:
xpath("//div[@class='negrita aut_sec']/a/text()")
Una vez tenemos las reglas de extracción, hay que describir como son las
direcciones de las páginas de libro a las que queremos aplicarlas. El formato
es http://www.epublibre.org/libro/detalle/6467, con el numero final variando
para cada libro:
Por último hay que crear la araña que usara scrapy en el directorio spiders:
#epublibre_spider.py
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import Selector
# Importar la clase donde almacenar los resultados
from epublibre_crawler.items import BookItem
class BookSpider(CrawlSpider):
# Nombre de la araña.
name = 'epublibre'
# Dominios en los que el crawler tiene permiso a acceder
allowed_domains = ['epublibre.org']
# La direccion de inicio para el crawler
start_urls = ['http://www.epublibre.org']
# Regla para diferenciar los enlaces de libros y función que se les aplica
rules = [Rule(SgmlLinkExtractor(allow=['/libro/detalle/\d+']), 'parse_book')]
def parse_book(self, response):
""" Parser para las pagina de detalle de los libros"""
sel = Selector(response)
# Creamos un nuevo libro y asignamos los valores extraidos a
# los campos correspondientes.
book = BookItem()
author = sel.xpath("//div[@class='negrita aut_sec']/a/text()").extract()
title = sel.xpath("//div[@id='titulo_libro']/text()[normalize-space()]").extract()
# Con Strip eliminamos tabulaciones y linea nueva.
book['title'] = title[0].strip("\t\n\r")
book['author'] = author[0].strip("\t\n\r")
return book
Antes de iniciar scrapy hay que modificar las configuración y limitar la
velocidad a la que los datos son accedidos, para no crear un ataque DOS:
# Scrapy settings for epublibre_scrapy project
#
# For simplicity, this file contains only the most important settings by
# default. All the other settings are documented here:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
#
BOT_NAME = 'epublibre_crawler'
SPIDER_MODULES = ['epublibre_scrapy.spiders']
NEWSPIDER_MODULE = 'epublibre_scrapy.spiders'
# Maximo una pagina cada minuto
DOWNLOAD_DELAY = 60
# Identificate
# USER_AGENT = 'epublibre_crawler (+http://www.tudominio.com)'
Finalmente solo queda ejecutar scrapy para que recolecte la información y la
guarde en un archivo en alguno de los formatos soportados (XML, JSON, CSV),
o hasta directamente en una base de datos usando una pipeline. En mi
caso JSON es el formato elegido:
$ scrapy crawl epublibre -o libros.json -t json
el nombre epublibre es el que asigné a la araña BookSpider en la
variable name.
El formato del archivo de salida es el siguiente:
[{"author": ["Ray Bradbury"], "title": ["Fahrenheit 451"]},
{"author": ["John Scalzi"], "title": ["Redshirts"]}
]
Nota: Este programa es sólo un ejemplo para explicar el funcionamiento de
scrapy, si realmente deseas extraer la información de epublibre.org, la propia
página distribuye un archivo CSV con todos los datos de los libros
disponibles.
Click to read and post comments
Una situación muy común en cualquier aplicación web es la necesidad de mostrar
miniaturas de las imágenes subidas por los usuarios. Esto se puede conseguir
usando CSS para escalar la imagen, cortarla, y hasta convertirla a blanco y
negro, pero es un desperdicio de ancho de banda.
En Django existen varias apps para simplificar la tarea, pero mi preferida
por su simplicidad y facilidad de integración es
easy-thumbnails
Instalación
Usa pip para instalar en paquete:
$ pip install easy_thumbnails
Añade easy_thumbnails a la lista de aplicaciones instaladas en el archivo
settings.py:
INSTALLED_APPS = (
...
'easy_thumbnails',
)
Uso
Easy-thumbnails funciona generando dinámicamente miniaturas desde las imágenes
originales. Cuando llega una petición y la miniatura no existe, o ha sido
modificada, una nueva miniatura es generada y salvada.
Para mostrar las miniaturas en los templates, es necesario usar los tags
proporcionados por easy_thumbnails, thumbnail y thumbnail-url:
{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<div class="producto">
<img src="{% thumbnail producto.foto 90x90 crop %}"/>
<h3>{{ producto.nombre }}</h3>
</div>
En el tag indicamos la imagen fuente, el tamaño al que deseamos convertir la
imagen, seguido de cualquier opción adicional, en este caso indicamos que corte
la imagen si es necesario para llegar al tamaño seleccionado.
También es posible indicar solo una de las dimensiones de la imagen, de manera
que la otra se escale para mantener las proporciones, sin deformar la imagen ni cortar los bordes.
{% load thumbnail %}
....
<div class="producto">
<img src="{% thumbnail producto.foto 90x0 %}"/>
<h3>{{ producto.nombre }}</h3>
</div>
Para simplificar la tarea y evitar errores, podemos crear un alias para las
distintas configuraciones de las miniaturas, para ello hay que crear un
diccionario llamado THUMBNAIL_ALIASES en settings.py, que contiene un las
opciones de cada alias:
THUMBNAIL_ALIASES = {
'': {
'producto': {'size': (90, 90), 'crop': True},
'cartel': {'size': (90, 0),},
},
}
Luego podemos usar estos alias con:
{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<div>
<img src="{% producto.foto|thumbnail_url:'cartel' %}"/>
<h3>{{ producto.nombre }}</h3>
</div>
ThumbnailerImageField
Este campo nos hace aún más fácil manejar las direcciones de las miniaturas en
en template, y permite gestionar cuando son generadas. Para usarlo solo es
necesario substituir en los modelos ImageField por ThumbnailerImageField:
from django.db import models
from easy_thumbnails.fields import ThumbnailerImageField
class Producto(models.Model):
"""Mi modelo de un producto"""
foto = ThumbnailerImageField()
text = models.TextField(text)
En los templates puedes obtener la dirección de la miniatura fácilmente
usado su alias:
{% load thumbnail %}
....
<div>
<img src="{{ producto.foto.cartel.url }}"/>
<h3>{{ producto.nombre }}</h3>
</div>
Si la aplicación requiere que las miniaturas sean generadas en el momento que
las imágenes son subidas, se pueden usar los manejadores de señales en
models.py:
from easy_thumbnails.signals import saved_file
from easy_thumbnails.signal_handlers import generate_aliases_global
saved_file.connect(generate_aliases_global)
Integración con Amazon S3
En caso de que necesites usar Amazon S3 para
almacenar imágenes, ya sea para servirlas directamente, o como backup,
configurar easy_thumbnails es muy sencillo.
Primero necesitamos instalar django-storage y la biblioteca boto de python para
S3.
$ pip install django-storage
$ pip install boto
Una vez instalados configuramos easy_thumbnails para indicarle que tiene que
usar S3 como almacenamiento, en settings.py hay que añadir:
THUMBNAIL_DEFAULT_STORAGE ='storages.backends.s3boto.S3BotoStorage'
THUMBNAIL_BASEDIR = '_miniaturas/'
Ademas de indicar que use S3, lo he configurado para que guarde las miniaturas
en un directorio aparte llamado '_miniaturas/', esto hace que sean más
sencillas de gestionar.
Tras esto configuramos Django para que también guarde la imagen original en S3,
añadimos las credenciales AWS, y el nombre del bucket a usar:
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
AWS_S3_SECURE_URLS = False # Usar http en lugar de https
AWS_QUERYSTRING_AUTH = False # authentication sencilla
AWS_ACCESS_KEY_ID = 'Tu_ACCESS_KEY_ID'
AWS_SECRET_ACCESS_KEY = 'Tu_SECRET_ACCESS_KEY'
AWS_STORAGE_BUCKET_NAME = 'nombre_bucket'
Si usas S3 es recomendable usar señales para que easy_thumbnails calcule las
miniaturas en el momento de creación de las imágenes.
Por último sincronizamos la base de datos
$ python manage.py syncdb
Click to read and post comments
Esta semana he estado desarrollando una webapp django en un ordenador sin SSD,
y el tiempo de ejecución era mucho mayor de lo normal, ya no recordaba lo
lentos que son los discos duros especialmente su acceso aleatorio.
Por supuesto me he puesto a investigar formas de acelerar el proceso.
Cuando estás desarrollando una aplicación django tienes tres opciones
como base de datos MySQL,
PostgreSQL, o SQLite.
A la hora de hacer tests SQLite es indiscutiblemente la más rápida, ya que
crea la base de datos en ram, pero no tiene soporte completo de ALTER TABLE,
por lo que no es posible hacer migraciones. MySQL y PostgreSQL en cambio
son lentas en la creación de bases de datos, pero soportan migraciones.
La solución perfecta es usar MySQL o PostgreSQL para el desarrollo, y usar
SQLite en los tests. Esto lo podemos conseguir modificando el archivo de
configuración cuando se van a ejecutar tests, para cambiar la base de datos a
sqlite3.
Primero creamos un archivo de configuración alternativo test_settings.py,
en el añadimos la configuración de la base de datos a usar en los tests:
# test_settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', #
'NAME': 'test_sqlitedb', # Ruta al archivo de la base de datos
}
}
Tras esto solo queda modificar settings.py para que cargue el archivo de
configuración cuando esté ejecutando tests, esto lo conseguimos comprobando si
los argumentos de la linea de comandos contiene la palabra "test". Al final
del archivo settings.py hay que añadir:
# ^^^^^^^^ Resto de configuracion settings.py ^^^^^^
# Cargar configuracion test_settings.py si se ejecuta manage.py test
import sys
if 'test' in sys.argv:
try:
from test_settings import *
except ImportError:
print "No se pudo encontrar el archivo test_settings.py"
El tiempo de ejecución de la aplicación usando PostgreSQL:
$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 1.047s
OK
Destroying test database for alias 'default'...
real 0m14.169s
user 0m1.204s
sys 0m0.136s
Y usando SQLite:
$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 0.299s
OK
Destroying test database for alias 'default'...
real 0m1.031s
user 0m0.872s
sys 0m0.100s
Una reducción del 90% (13 segundos), nada mal para una modificación tan sencilla.
Click to read and post comments
ImageField proporciona una buena abstracción para la gestión imágenes,
hace fácil la subida de los archivos a la pagina, y proporciona tags para
mostrarlas en los template.
Pero ciertos comportamientos por defecto del campo, hacen imposible usarlo sin
modificaciones en prácticamente cualquier proyecto django.
Estas son algunas de las modificaciones que suelo hacer en casi cualquier
modelo con un campo ImageField.
Cambiar el nombre del archivo antes de guardarlo
Cuando ImageField salva la imagen, lo hace en un archivo con el mismo nombre
que el archivo original, esto es un problema puesto que si se salvan dos
imágenes con el mismo nombre una sobreescribe a la otra.
Este comportamiento es admisible un blog, donde el autor puede controlar
el nombre de cada imagen, en cambio en una página donde los usuarios puedan
subir imágenes, no tardaría en ser un problema grave.
La solución es usar el parámetro de ImageField upload_to, para
proporcionar un nombre de archivo aleatorio con el que salvar la imagen.
from django.db import models
from uuid import uuid4
from datetime import date
import os
class Post(models.Model):
def _generar_ruta_imagen(instance, filename):
# El primer paso es extraer la extension de la imagen del
# archivo original
extension = os.path.splitext(filename)[1][1:]
# Generamos la ruta relativa a MEDIA_ROOT donde almacenar
# el archivo, usando la fecha actual (año/mes)
ruta = os.path.join('Imagenes', date.today().strftime("%Y/%m"))
# Generamos el nombre del archivo con un identificador
# aleatorio, y la extension del archivo original.
nombre_archivo = '{}.{}'.format(uuid4().hex, extension)
# Devolvermos la ruta completa
return os.path.join(ruta, nombre_archivo)
imagen = models.ImageField(upload_to=_generar_ruta_imagen)
text = models.TextField(max_lenght=1000)
La función _generar_ruta_imagen es llamada la primera vez que se salva la
imagen para generar la ruta.
Si estudias su código, verás que ademas de generar el nombre del archivo,
generamos la ruta donde es almacenado, esta ruta se genera con la fecha en el
momento de salvado, de manera que cambia con el paso del tiempo.
Con esto se evita que miles de imágenes se guarden en un solo directorio,
haciéndolo difícil de listar y manipular.
Usar un Mixin para añadir imágenes a varios modelos
En el caso de que tengamos varios modelos que necesiten una imagen, podemos
crear una clase abstracta que proporciona un campo imagen, que genere de
forma correcta las rutas y nombres. Luego solo tenemos que heredar de ella en
cada modelo que necesite esa funcionalidad.
from django.db import models
from uuid import uuid4
from datetime import date
import os
class ImagenMixin(models.Model):
def _generar_ruta_imagen(instance, filename):
# El primer paso es extraer la extension de la imagen del
# archivo original
extension = os.path.splitext(filename)[1][1:]
# Generamos la ruta relativa a MEDIA_ROOT donde almacenar
# el archivo, se usa el nombre de la clase y la fecha actual.
directorio_clase = instance.__class__.__name__
ruta = os.path.join('imagenes', directorio_clase,
date.today().strftime("%Y/%m"))
# Generamos el nombre del archivo con un identificador
# aleatorio, y la extension del archivo original.
nombre_archivo = '{}.{}'.format(uuid4().hex, extension)
# Devolvermos la ruta completa
return os.path.join(ruta, nombre_archivo)
imagen = models.ImageField(upload_to=_generar_ruta_imagen)
class Meta:
abstract = True
class Post(ImagenMixin):
text = models.TextField(max_lenght=1000)
class Libro(ImagenMixin):
titulo = models.CharField(max_length=100)
El único cambio está en _generar_ruta_imagen de la clase abstracta,
donde se añade a la ruta de salvado el nombre de la clase.
De esta manera se separan las imágenes salvadas por cada clase en un
directorio distinto.
Por ejemplo los modelos Post y Libro guardarían sus imágenes en:
- /MEDIA_ROOT/imagenes/Post/año/mes/
- /MEDIA_ROOT/imagenes/Libro/año/mes/
Mostrar imágenes en el interfaz Admin
Si usas admin para añadir contenido a tu página web, es realmente incómodo no
poder ver las imágenes directamente en su interfaz. Para solucionarlo creamos
un método en el modelo, que devuelve un tag <img> apuntando a la dirección de
la imagen:
from django.db import models
from django.utils.safestring import mark_safe
class Post(models.Model):
imagen = models.ImageField()
text = models.TextField(max_length=1000)
def imagen_admin(self):
if self.image:
# Marcamos imagen como safe para evitar escape automatico
return mark_safe(u'<img src="%s" />' % self.imagen.url))
else:
return '(Sin imagen)'
# Para cambiar el nombre del campo en pantalla
imagen_admin.short_description = 'Imagen'
# En lugar de mark_safe podemos añadir:
imagen_admin.allow_tags = True
Y modificamos el ModelAdmin del modelo para que muestre la imagen. Hay que
asegurarse de añadir el nuevo método a readonly_fields:
from django.contrib import admin
from blog.models import Post
class PostAdmin(admin.ModelAdmin):
# imagen_admin tiene que ser siempre readonly, si queremos modificar
# la imagen hay que hacerlo a traves del campo imagen.
readonly_fields = ('imagen_admin',)
fields = ('imagen_admin', 'imagen', 'text',)
admin.site.register(Post, PostAdmin)
Si no quieres modificar tu modelo, puedes crear un método similar
directamente en ModelAdmin:
from django.contrib import admin
from blog.models import Post
class PostAdmin(admin.ModelAdmin):
def imagen_admin(self, obj):
return u'<img src="%s" />' % obj.image.url
imagen_admin.allow_tags = True
readonly_fields = ('imagen_admin',)
fields = ('imagen_admin', 'text')
admin.site.register(Post, PostAdmin)
Por último si estás usando el mixin de la sección anterior, ese es el mejor
lugar donde añadir el método.
Eliminar el archivo de la imagen al borrar el modelo
El comportamiento por defecto de ImageField, es no borrar el archivo cuando el
modelo es eliminado, para borrarlo es necesario conectarse a la señal
pre_delete que se envía antes de la eliminación de cualquier modelo, y
hacerlo manualmente.
from django.db import models
from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver
class Post(models.Model):
imagen = models.ImageField()
text = models.TextField(max_length=1000)
# Usamos el Decorador receiver para ejecutar nuestra función
# cuando el Post el borrado.
@receiver(pre_delete, sender=Post)
def post_pre_delete_handler(sender, instance, **kwargs):
instance.imagen.delete(False)
Ten en cuenta que si borras el archivo con el modelo, puedes perder datos
si hay algún accidente, si el numero de imágenes almacenado no es grande
es mejor no borrarlas. Si el número es grande, puedes usar una tarea asíncrona
que borre los archivos huérfanos tras borrar los modelos, esto lo puedes
conseguir con celery, cron, o similar.
Click to read and post comments
Si estás desarrollando una aplicación web, algún día tendrás que dar el paso
y hacerla pública. Inicialmente puedes usar un servicio como
Heroku pero si tienes éxito, no te quedará mas
remedio que gestionar tus propios servidores o VPS.
Por fortuna desplegar una aplicación django usando nginx y gunicorn, es
más sencillo de lo que podría parecer. En esta pequeña guia trato de describir
el proceso paso a paso:
1 - Crear un usuario para la aplicación django
El primer paso es crear un usuario con el que ejecutar la aplicación
Django, esto nos permite organizar con facilidad un servidor donde estemos
ejecutando varias aplicaciones, proporciona separación de privilegios, y limita
el posible daño que pueda hacerse al sistema si la aplicación es comprometida.
Creamos un grupo al que pertenecerán todos los usuarios de las aplicaciones
django, y un usuario al que le asignamos el nombre de la aplicación.
$ sudo addgroup --system webapps
$ sudo adduser --system --ingroup webapps --home /webapps/appname appname
Al usar en modificador --system, al usuario se le asigna /bin/false como
shell, y no tiene clave, por lo que no puede hacer login. Aunque esto es
una buena medida de seguridad, es muy incomodo mientras se está configurando
el sistema, así que asignamos un shell temporalmente:
$ sudo chsh -s /bin/bash appname
Esto nos permite usar sudo su appname para seguir con la instalación como
el nuevo usuario.
2 - Instalar y configurar virtualenv
Virtualenv es la herramienta que nos permite aislar los paquetes requeridos
por las aplicaciones, de manera que si dos aplicaciones necesitan paquetes
que están en conflicto, no interfieran la una con la otra como ocurriría si
instalásemos todos los paquetes directamente en el sistema.
Instalamos virtualenv con:
$ sudo apt-get install python-virtualenv python-pip
Una vez instalado, hacemos login con el usuario que hemos creado, y dentro de
su directorio usamos virtualenv para generar un nuevo entorno virtual:
$ sudo su appname
$ cd
$ mkdir virtualenvs
$ virtualenv --no-site-packages virtualenvs/app_env
$ source virtualenvs/app_env/bin/activate
Dentro del entorno, hay que instalar todos los paquetes que sean necesarios
para la aplicación, puedes hacerlo uno a uno, o en bloque si tienes el archivo
requirements.txt:
(app_env)$ pip install django
(app_env)$ pip install django-countries
(app_env)$ pip install django-mptt
(app_env)$ ....
(app_env)$ pip install -r requirements.txt
Si quieres profundizar en el funcionamiento de virtualenv sigue este
tutorial
3 - Gunicorn + Supervisor
Gunicorn es el servidor WSGI que se encarga de servir la aplicación, pero
necesita un programa que lo inicie al arranque, y lo monitorice para
reiniciarlo si hay algún problema. La mejor solución es usar el gestor de
procesos como supervisor.
La instalación es sencilla:
$ sudo apt-get install supervisor
Para instalar gunicorn el método más sencillo es hacerlo dentro del entorno
virtual de tu aplicación django.
$ source virtualenvs/app_env/bin/activate
(app_env)$ pip install gunicorn
Una vez todo está instalado, creamos un archivo de configuración para gunicorn
gunicorn_conf.py en el directorio HOME del usuario, en el que indicamos la
dirección y puerto en los que estará escuchando gunicorn:
# gunicorn_conf.py
workers = 3
bind = '127.0.0.1:9000'
A partir de aquí ya podemos salir del entorno virtual de la aplicación:
El siguiente paso es crear el archivo de configuración de supervisor en
/etc/supervisor/conf.d/appname.conf:
[program:appname]
command=/webapps/appname/django_app/run_gunicorn.sh
directory = /webapps/appname/django_app/
user=appname
autostart=true
autorestart=true
priority=991
stopsignal=KILL
Tras esto creamos en el directorio de la aplicación el script
run_gunicorn.sh que etablece el entorno virtualenv para la aplicación y
despuer ejecuta gunicorn:
#!/bin/bash
source /webapp/appname/virtualenvs/app_env/bin/activate
exec /webapp/appname/virtualenvs/app_env/bin/gunicorn -c /webapps/appname/gunicorn_conf.py django_app.wsgi:application
Una vez configurado indicamos a supervisor que debe iniciar el nuevo servicio:
$ sudo supervisorctl start reread
appname: available
$ sudo supervisorctl update
appname: added process group
$ sudo supervisorctl status appname
appname RUNNING pid 21710, uptime 0:00:07
Para detener e iniciar la aplicación podemos usar:
$ sudo supervisorctl stop appname
appname: stopped
$ sudo supervisorctl start appname
appname: started
Llegado a este punto si todo funciona correctamente, ya podemos desactivar
el shell del usuario creado para la aplicación.
$ sudo -s /bin/false appname
Si prefieres que sea posible hacer login con el usuario, es recomendable que
le asignes una clave.
4 - Nginx
Usamos Nginx para hacer de pasarela entre los clientes y gunicorn, y para
servir los archivos estáticos de la aplicación. Para instalarlo:
$ sudo apt-get install nginx
Para configurar nginx, hay que crear un archivo /etc/ngix/sites-available/,
en el especificaremos donde se debe conectar a gunicorn en la sección upstream,
y la ruta a los archivos estáticos de tu aplicación en la sección server:
upstream django_app_server {
# Dirección en la que está escuchando gunicorn
server 127.0.0.1:9000 fail_timeout=0;
}
server {
# listen 80 default deferred; # for Linux
# listen 80 default accept_filter=httpready; # for FreeBSD
listen 80 default;
client_max_body_size 4G;
server_name www.dominio.com;
# ~2 seconds is often enough for most folks to parse HTML/CSS and
# retrieve needed images/icons/frames, connections are cheap in
# nginx so increasing this is generally safe...
keepalive_timeout 5;
# Ruta a tus archivos estaticos.
location /static/ {
alias /webapps/appname/django_app/static/;
autoindex on;
}
location / {
# an HTTP header important enough to have its own Wikipedia entry:
# http://en.wikipedia.org/wiki/X-Forwarded-For
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# enable this if and only if you use HTTPS, this helps Rack
# set the proper protocol for doing redirects:
# proxy_set_header X-Forwarded-Proto https;
# pass the Host: header from the client right along so redirects
# can be set properly within the Rack application
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
# set "proxy_buffering off" *only* for Rainbows! when doing
# Comet/long-poll stuff. It's also safe to set if you're
# using only serving fast clients with Unicorn + nginx.
# Otherwise you _want_ nginx to buffer responses to slow
# clients, really.
# proxy_buffering off;
# Try to serve static files from nginx, no point in making an
# *application* server like Unicorn/Rainbows! serve static files.
proxy_pass http://django_app_server;
}
}
Descargar
Una vez configurado, hay que enlazar el archivo que hemos creado en
/etc/nginx/sites-available/ a /etc/nginx/sites-enabled/, de esta manera
nginx empezará a usar la configuración:
$ sudo ln -s /etc/nginx/sites-available/midominio /etc/nginx/sites-enabled/midominio
Por último reiniciamos nginx:
$ sudo service nginx restart
Nota: Los autores de gunicorn tiene un ejemplo más completo de un archivo de
configuración
nginx.conf
Alternativas
Por supuesto esta no es la única opción para desplegar django, ni es la mejor
para todas las situaciones. Si estas administrando múltiples aplicaciones,
y/o múltiples servidores quizás Docker o
Dokku se ajuste mejor a tus necesidades.
Y como comenté al inicio Heroku es una buena
alternativa para páginas con poco tráfico.
Click to read and post comments