Manejo de cargas de archivos con Django

En esta guía, veremos cómo cargar archivos en una aplicación web de Django, usando Python, creando una pequeña aplicación y explorando opciones de tipo de archivo y almacenamiento en el camino.

Introducción

La World Wide Web facilitó la transferencia de grandes cantidades de datos entre computadoras en red, y es una comunidad que crea y comparte datos en abundancia. Estos datos pueden tomar varias formas y formas, y algunos formatos comunes interpretables por humanos son imágenes, videos y archivos de audio.

Los usuarios están tan acostumbrados a compartir archivos dentro de una amplia variedad de software, que su novedad está muy lejos y su funcionalidad a menudo se considera estándar.

En esta guía, veremos cómo cargar un archivo con Python a una aplicación web basada en Django.

Los archivos que se cargan se pueden procesar adicionalmente en varias formas, o se pueden dejar en su estado original. La carga de archivos también plantea una cuestión de almacenamiento (dónde terminan los archivos), así como de visualización (cómo se pueden recuperar y mostrar). A lo largo de la guía, tomaremos en consideración estas preguntas, creando un pequeño proyecto que ofrece al usuario la capacidad de cargar archivos en una aplicación web de Django.

Configuración del proyecto

Construiremos un pequeño proyecto donde podamos implementar las funcionalidades de carga, almacenamiento y visualización de archivos de Django, con una base de datos, sin embargo, almacenando las imágenes en un disco duro.

Supongamos que vivimos en un universo imaginario donde vivimos junto a las criaturas mágicas de los libros de Harry Potter, y los zoólogos mágicos de nuestro mundo necesitan una aplicación para realizar un seguimiento de la información sobre cada criatura mágica que estudian. Crearemos un formulario a través del cual puedan registrar descripciones e imágenes para cada bestia, luego representaremos ese formulario, almacenaremos la información y se la mostraremos al usuario cuando sea necesario.

Si no está familiarizado con Django y sus módulos, como django-admin, puede leer la guía general para crear API REST que cubren los elementos básicos de Django. A lo largo de esta guía, asumiremos conocimientos básicos de Django y pasaremos rápidamente por el proceso de configuración, aunque, si desea obtener una comprensión más profunda del proceso de creación de proyectos, lea nuestra Guía para crear una API REST en Python con Django!

Comenzamos creando un ambiente virtual para evitar que nuestras dependencias causen problemas de desajuste de versión con otros proyectos. Este paso es opcional, pero muy recomendable y se considera una buena práctica para mantener limpios los entornos de Python. Vamos a crear un directorio que actuará como contenedor para el entorno.

Abra su símbolo del sistema/shell y dentro del directorio que acabamos de crear, ejecute:

1
2
3
4
5
$ mkdir fileupload
$ cd fileupload
$ python -m venv ./myenv
# OR
$ python3 -m venv ./myenv

Ahora que nuestro entorno virtual ha sido creado, todo lo que queda por hacer es activarlo, ejecutando el script activate:

1
2
3
4
5
6
# Windows
$ myenv/Scripts/activate.bat
# Linux
$ source myenv/Scripts/activate
# MacOS
$ source env/bin/activate

Una vez que el entorno está activado, si instalamos dependencias, solo serán aplicables a ese entorno y no colisionarán con otros entornos, o incluso con el entorno del sistema. Aquí, podemos instalar Django a través de pip:

1
$ pip install "Django==3.0.*"

Ahora, vamos a crear un proyecto, llamado fantasticbeasts a través del comando startproject del módulo django-admin. Una vez que se ha creado el esqueleto de un proyecto, podemos movernos a ese directorio e iniciar la aplicación a través de startapp:

1
2
3
$ django-admin startproject fantasticbeasts
$ cd fantasticbeasts
$ django-admin startapp beasts

Y finalmente, registremos esta aplicación en el archivo fantasticbeasts/settings.py, agregándola a la lista de INSTALLED_APPS:

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
    'beasts',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

¡Impresionante! Ahora estamos listos. Podemos definir un modelo simple para una ‘Bestia’, crear un formulario y una plantilla para mostrarlo a un usuario final, así como manejar los archivos que envían con el formulario.

Subiendo archivos con Django

Creación del modelo

Comencemos definiendo un modelo de Bestia, que coincide directamente con una tabla de base de datos. A continuación, se puede crear un formulario para representar una pizarra en blanco de este modelo, lo que permite al usuario completar los detalles. En el archivo beasts/models.py, podemos definir un modelo que extienda la clase models.Model, que luego hereda la funcionalidad para guardarse en la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.db import models

class Beast(models.Model):
    MOM_CLASSIFICATIONS = [
    ('XXXXX', 'Known Wizard  Killer'),
    ('XXXX', 'Dangerous'),
    ('XXX', 'Competent wizard should cope'),
    ('XX', 'Harmless'),
    ('X', 'Boring'),
 ]
    name = models.CharField(max_length=60)
    mom_classification = models.CharField(max_length=5, choices=MOM_CLASSIFICATIONS)
    description = models.TextField()
    media = models.FileField(null=True, blank=True)

Cada bestia tiene un ’nombre’, ‘descripción’, ‘medios’ que lo acompañan (avistamientos de la bestia), así como una ‘clasificación de mamá’ (M.O.M significa Ministerio de Magia).

media es una instancia de un FileField que se inicializó con el argumento null establecido en True. Esta inicialización le permite a la base de datos saber que está bien que el campo “medios” sea nulo si el usuario que ingresa los datos simplemente no tiene ningún medio para adjuntar. Dado que estaremos asignando este modelo a un formulario, y Django se encarga de la validación por nosotros, debemos informarle a Django que la entrada del formulario para el medio puede estar en blanco, por lo que no 't generar ninguna excepción durante la validación. null se refiere a la base de datos, mientras que blank se refiere a la validación del usuario y, en general, querrá que estos dos se establezcan en el mismo valor para mantener la coherencia.

{.icon aria-hidden=“true”}

Nota: Si desea forzar la adición de medios por parte del usuario, establezca estos argumentos en False.

Un FileField de forma predeterminada solo manejará un archivo y permitirá al usuario cargar un solo elemento desde su sistema de archivos. En una sección posterior, también veremos como subir varios archivos.

Creación del formulario modelo

Una vez que nuestro modelo esté definido, lo vincularemos a un formulario. No necesitamos hacer esto manualmente en el front-end, ya que Django puede iniciar esta funcionalidad por nosotros:

1
2
3
4
5
6
7
from django.forms import ModelForm
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
        fields = '__all__'

Acabamos de crear un BeastForm y le vinculamos el modelo Beast. También configuramos fields en __all__ para que todos los campos de nuestro modelo se muestren cuando lo usemos en una página HTML. Puede modificar individualmente los campos aquí si desea que algunos permanezcan ocultos, sin embargo, para nuestro modelo simple, queremos mostrarlos todos.

Registro de modelos con administrador

Django crea automáticamente un sitio de administración para que los desarrolladores lo usen durante todo el proceso de desarrollo. Aquí, podemos probar nuestros modelos y campos sin tener que activar las páginas nosotros mismos. Sin embargo, para los usuarios, querrá crearlos y deshabilitar el sitio web de administración antes de publicarlo.

Registremos nuestro modelo en el sitio web de administración agregándolo al archivo beasts/admin.py:

1
2
3
4
from django.contrib import admin
from .models import Beast

admin.site.register(Beast)

Registro de rutas URL

Con la estructura de la aplicación lista, un modelo definido y registrado, así como vinculado a un formulario, configuremos las rutas URL que permitirán que un usuario use esta aplicación. Para hacer esto, vamos a crear un archivo urls.py dentro de nuestra aplicación. Luego podemos continuar e "incluir" su contenido en el archivo urls.py a nivel de proyecto.

Nuestro beasts/urls.py se verá así:

1
2
3
4
5
6
from django.urls import path
from .import views

urlpatterns = [
    path("", views.addbeast,  name='addbeast')
 ]

Y el nivel de proyecto [urls.py] tendrá esto agregado:

1
2
3
urlpatterns = [
    path("", include("reviews.urls"))
]

Estamos agregando una cadena vacía para nuestra URL simplemente porque este es un proyecto de bolsillo y no hay necesidad de complicarlo. Aún no hemos creado una vista, pero registramos su nombre aquí antes de su creación. Vamos a crear la plantilla HTML y la vista views.addbeast a continuación.

Creación de una plantilla para mostrar nuestro formulario

Para guardar nuestras plantillas, vamos a crear una carpeta templates en nuestro directorio beasts. El nombre no es negociable porque Django buscará plantillas HTML solo en las carpetas llamadas templates.

Dentro de nuestra nueva carpeta, agreguemos un archivo entry.html que tenga un <formulario> que acepte los campos pertenecientes a una Beast:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    <form action="/" method="POST" enctype="multipart/form-data">
        {% csrf_token %}
        {% for entry in form %}
           <div>
                {{ entry.label_tag }}
           </div>
           <div>
               {{entry}}
           </div>
        {% endfor %}
        <button>
            Save!
        </button>
    </form>
</body>
</html>

El atributo action="/" apunta al controlador de solicitudes que estaremos presionando cuando el usuario seleccione el botón "¡Guardar!". La entrada del formulario dicta cómo se codifican los datos, por lo que configuramos enctype en un tipo multipart/form-data para permitir la carga de archivos. Siempre que agregue una entrada de tipo "archivo" a un formulario de Django, tendrá que establecer enctype en multipart/form-data.

El {% csrf_token %} es otro imprescindible para cualquier formulario con action = "POST". Es un token único que Django crea amablemente para cada cliente para garantizar la seguridad al aceptar solicitudes. Un token CSRF es único para cada solicitud ‘POST’ de este formulario, y hacen que los ataques CSRF sean imposibles.

Los ataques CSRF consisten en usuarios maliciosos que falsifican una solicitud en lugar de otro usuario, generalmente a través de otro dominio, y si falta un token válido, se rechaza la solicitud al servidor.

La variable form que estamos iterando en el ciclo for each ({% for input in form %}) será pasada a esta plantilla HTML por la vista. Esta variable es una instancia de nuestro BeastForm y viene con algunos trucos geniales. Usamos entry.label_tag, que nos devuelve la etiqueta para ese campo de formulario modelo (la etiqueta será el nombre del campo a menos que se especifique lo contrario), y envolvemos el campo de formulario en un div para que nuestro formulario se vea decente.

Crear una vista para renderizar nuestra plantilla

Ahora, vamos a crear una vista para representar esta plantilla y conectarla a nuestro back-end. Comenzaremos importando las clases render y HttpResponseRedirect, ambas clases integradas de Django, junto con nuestro objeto BeastForm.

En lugar de una vista basada en clases, podemos optar por hacer una vista basada en funciones que sirva bien para prototipos simples y demostraciones como esta.

Si la solicitud entrante es una solicitud POST, se crea una nueva instancia BeastForm con el cuerpo de la solicitud POST (los campos) y los archivos enviados a través de la solicitud. Django deserializa automáticamente los datos del cuerpo en un objeto e inyecta request.FILES como nuestro campo de archivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.shortcuts import render
from .forms import BeastForm
from django.http import HttpResponseRedirect

def entry(request):
    if request.method == 'POST':
        form = BeastForm(request.POST, request.FILES)
        
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/") 
    else:
        form = BeastForm()

    return render(request, "entry.html", {
        "form": form
    })

Para validar la entrada, ya que puede no ser válida, podemos usar el método is_valid() de la instancia BeastForm, limpiando el formulario si no es válido. De lo contrario, si el formulario es válido, lo guardamos en la base de datos a través del método save() y redirigimos al usuario a la página de inicio (que también es nuestra página entry.html), lo que le pide al usuario que ingrese otra bestia información de .

¿Dónde están los archivos?

Nota: A través de este enfoque, los archivos se guardan en la base de datos y no se requiere manejo de archivos. Aunque funciona, esta no es una estrategia recomendada y la rectificaremos con un sistema de manejo de archivos adecuado en la siguiente sección.

Por ahora, hagamos migraciones y migremos para confirmar cambios en el esquema del modelo (ya que no lo hemos hecho antes). Una vez que ejecutamos el proyecto, podemos ver cómo se ve todo esto en el servidor de desarrollo. Desde la terminal, mientras el entorno virtual aún está activo, ejecute:

1
2
3
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

Ahora, una vez que presionemos http://127.0.0.1:8000/ usando un navegador, debería ver algo como esto:

django model form

Puede continuar y completar el formulario con alguna entrada aleatoria y agregar un archivo; cualquier tipo de archivo servirá ya que llamamos al campo "medios" pero le asignamos un FileField que es genérico.

Nota: Puede aplicar ciertos tipos de archivos, como imágenes a través de Django, que veremos una vez que cubramos un sistema de almacenamiento de archivos más válido y el manejo de varios archivos en lugar de solo uno.

¡Después de enviar el formulario, puede ver sus datos en la base de datos a través de la página de administración!

Almacenamiento de archivos en un HDD en lugar de una base de datos

Por el momento, nuestro código es capaz de almacenar los archivos en la base de datos. Sin embargo, esta no es una práctica deseable. Con el tiempo, nuestra base de datos se volverá "gorda" y lenta, y no queremos que eso suceda. Las imágenes no se han almacenado en bases de datos como blobs desde hace un tiempo y, por lo general, guardará las imágenes en su propio servidor donde se aloja la aplicación, o en un servidor o servicio externo como el de AWS. S3.

If you'd like to read more about uploading files to services such as AWS S3 - read our Guía para cargar archivos en AWS S3 en Python con Django!

Veamos cómo podemos almacenar los archivos cargados en el disco, en una pequeña carpeta agradable debajo de nuestro proyecto. Para albergarlos, agreguemos una carpeta uploads debajo de beasts y modifiquemos el campo multimedia BeastForm para apuntar a una carpeta en lugar de una base de datos:

1
media = models.FileField(upload_to="media", null=True, blank=True)

Hemos configurado la carpeta de destino de FileField en "media", que aún no existe. Dado que presumiblemente se pueden cargar otros archivos, la carpeta uploads tendrá un subdirectorio llamado "media" para que los usuarios carguen imágenes de bestias.

Para que Django sepa dónde está este directorio "media", lo agregamos al archivo settings.py:

1
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads/')

os.path.join(BASE_DIR, 'uploads/') agrega "/uploads" a BASE_DIR, la variable integrada que contiene la ruta absoluta a nuestra carpeta de proyecto. MEDIA_ROOT le dice a Django dónde residirán nuestros archivos.

Guardemos todos los cambios que hemos realizado y una vez que apliquemos nuestras migraciones, Django creará una carpeta llamada "media", como en [upload_to="media"], en uploads .

Todos los archivos enviados se guardarán en esa carpeta a partir de entonces. ¡La sobrecarga de la base de datos está arreglada!

Subir varios archivos con Django

No se requiere mucho trabajo adicional para manejar la carga de múltiples archivos. Todo lo que tenemos que hacer es dejar que el formulario de nuestro modelo sepa que está bien que el campo multimedia admita más de una entrada.

Hacemos esto agregando un campo widgets en nuestro BeastForm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.forms import ModelForm, ClearableFileInput
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
        fields = '__all__'
        widgets = {
            'media': ClearableFileInput(attrs={'multiple': True})
        }

Ahora, cuando está en la página entry.html, un usuario puede seleccionar varios archivos y la propiedad request.FILES contendrá más archivos en lugar de uno.

Hacer cumplir los archivos de imagen con Django usando ImageField

Django define un tipo de campo adicional: un ImageField, que puede limitar la entrada del usuario a archivos de imagen. Hemos recopilado diferentes tipos de archivos para la documentación de nuestras bestias, pero la mayoría de las veces, en nuestras aplicaciones, le pediremos al usuario que ingrese un archivo específico.

Intercambiemos nuestro FileField con un ImageField:

1
media = models.ImageField(upload_to="media", null=True, blank=True,)

El ImageField está en las imágenes Pillow, que es una biblioteca de Python ampliamente utilizada para manejar y manipular imágenes, por lo que si aún no lo tiene instalado, se le solicitará una excepción:

1
2
Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".

Sigamos adelante y sigamos los consejos de la terminal. Salga del servidor por un momento para ejecutar:

1
$ python -m pip install Pillow

Ahora, si seguimos adelante y hacemos y aplicamos nuestras migraciones y ejecutamos nuestro servidor de desarrollo, veremos que cuando intentamos cargar un archivo, nuestras opciones se limitan a las imágenes.

Visualización de imágenes cargadas

Estamos muy cerca de la meta. Veamos cómo podemos recuperar y mostrar nuestras imágenes almacenadas y dar por terminado el día.

Continúe y abra su archivo beasts/views.py. Cambiaremos nuestra cláusula if para que cuando un formulario se envíe correctamente, la vista no vuelva a cargar la página, sino que nos redirija a otra, que contendrá una lista de todas las bestias y su información, junto con su imagen asociada. :

1
2
3
 if form.is_valid():
      form.save()
      return HttpResponseRedirect("/success") 

Ahora avancemos y creemos una vista para representar la página de éxito. Dentro de nuestro archivo beasts/views.py, inserte:

1
2
3
4
5
def success(request):
    beasts = Beast.objects.order_by('name')
    return render(request, "success.html", {
        "beasts": beasts
    })

En nuestra página de "éxito", enumeraremos los nombres y las imágenes de las bestias en nuestra base de datos. Para hacer eso, simplemente recopilamos los objetos Beast, los ordenamos por su nombre y los representamos en la plantilla success.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    {% for beast in beasts %}
       <div>
            {{ beast.name }}
       </div>
       {% if beast.media %}
        <div>
            <img src="{{ beast.media.url }}" width="500" height=auto alt="">
        </div>
       {% endif %}
    {% endfor %}   
</body>
</html>

Ya hemos mencionado que el trabajo de la base de datos no es almacenar archivos, su trabajo es almacenar las rutas a esos archivos. Cualquier instancia de FileField o ImageField tendrá un atributo de URL que apunta a la ubicación del archivo en el sistema de archivos. En una etiqueta <img>, alimentamos este atributo al atributo src para mostrar las imágenes de nuestras bestias.

De forma predeterminada, la seguridad de Django se activa para evitar que entreguemos archivos del proyecto al exterior, lo cual es una verificación de seguridad bienvenida. Sin embargo, queremos exponer los archivos en el archivo "media", por lo que tendremos que definir una URL de medios y agregarla al archivo urls.py:

En el archivo settings.py, agreguemos MEDIA_URL:

1
MEDIA_URL = "/beast-media/"

Aquí, /nombre-entre/ puede ser lo que quieras, aunque tiene que estar entre comillas y barras. Ahora, modifique el archivo urls.py a nivel de proyecto para incluir una carpeta estática que sirva archivos estáticos:

1
2
3
4
5
6
7
8
9
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("ency.urls"))
]  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

La función static() asigna MEDIA_URL a la ruta real donde residen nuestros archivos, MEDIA_ROOT. Las solicitudes que intentan llegar a cualquiera de nuestros archivos pueden obtener acceso a través de esta MEDIA_URL, que se antepone automáticamente al atributo [url] de las instancias FileField e ImageField.

Si guardamos nuestros cambios y vamos a nuestro servidor de desarrollo, ahora veremos que todo funciona sin problemas.

{.icon aria-hidden=“true”}

Nota: Este método solo se puede usar en el desarrollo y solo si MEDIA_URL es local.

If you're not saving the files to your HDD, but a different service, read our Guía para servir archivos estáticos en Python con Django, AWS S3 y WhiteNoise!

Conclusión

En esta guía, hemos cubierto cómo cargar archivos, almacenar archivos y, finalmente, servir archivos con Django.

Hemos creado una pequeña aplicación que, además de con fines educativos, no es muy útil. Sin embargo, debería ser un trampolín sólido para comenzar a experimentar con la carga de archivos.