Saltar a contenido

Carga de archivos#

iThome Ironman 2024

Este es el artículo número 23 de la serie de tutoriales de Django Ninja. ¡Es hora de comenzar a explorar las funciones avanzadas!

En los servicios web modernos, la carga de archivos es un escenario muy común.

Ya sea que los usuarios suban fotos o adjunten archivos, la carga de archivos es una funcionalidad indispensable.

Este artículo explica cómo implementar la función de carga de imágenes en Django Ninja, tomando como ejemplo la API de usuario para «subir avatar» (a partir de aquí lo llamaremos avatar, porque «foto de perfil» suena un poco demasiado tierno 🥹) para guiarte paso a paso en este proceso.

Todos los cambios de código de este artículo se pueden consultar en este PR.

Proyecto de ejemplo en GitHub#

👉 Django-Ninja-Tutorial


Sin embargo, antes de eso, primero debemos entender cuáles son los temas de este capítulo.

Introducción al Capítulo 6: «Funciones avanzadas de API»#

Para los proyectos de API, las funciones avanzadas nos ayudan a enfrentar los desafíos de escenarios complejos y proyectos a gran escala.

Aunque esta es una guía para principiantes, aún cubriremos algunas funciones avanzadas comunes. Estas funciones no solo mejoran la flexibilidad de la API, sino que también potencian el rendimiento del sistema y la experiencia del usuario.

Este capítulo consta de 5 artículos que presentan 3 funciones avanzadas comunes:

Estas técnicas no solo son cruciales para proyectos a gran escala, sino que también te permiten responder de manera efectiva a requerimientos cambiantes durante el desarrollo de APIs.


Una vez comprendidos los puntos clave de este capítulo, comencemos a hablar sobre la primera función: la carga de archivos.

El protagonista de la carga de archivos: UploadedFile#

En Django Ninja, podemos usar UploadedFile para recibir los archivos subidos. Es un envoltorio del UploadedFile de Django, y ambos son básicamente muy similares.

Visión general de UploadedFile#

UploadedFile es el objeto central de Django para procesar la carga de archivos. Cuando un usuario sube un archivo, Django lo empaqueta automáticamente en una instancia de UploadedFile, lo que nos facilita el procesamiento y almacenamiento posterior del archivo.

El objeto UploadedFile tiene muchas propiedades, entre las cuales las más utilizadas son:

  • name: El nombre del archivo subido. Se puede utilizar para obtener el nombre original del archivo.
  • size: El tamaño del archivo (calculado en bytes). Podemos usarlo para validar el tamaño del archivo.
  • content_type: El tipo MIME del archivo. Esto es muy útil para validar el formato del archivo subido, como asegurarse de que sea un formato de imagen. ¡Lo usaremos más adelante!
  • read(): Se utiliza para leer el contenido del archivo. Cuando se requiere un procesamiento de archivos personalizado, podemos usar este método para obtener los datos binarios del archivo.
  • chunks(): Cuando el archivo es extremadamente grande, usar este método permite leer el archivo en fragmentos, evitando un consumo excesivo de memoria.

Estas características hacen que UploadedFile sea extremadamente flexible para manejar diversas necesidades de carga, desde simples cargas de imágenes hasta el procesamiento de archivos de gran tamaño.

Muy bien, con respecto a la carga de archivos, comprender el componente central UploadedFile es suficiente.

Antes de comenzar a escribir el código para la API de «subir avatar», debemos realizar algunos «preparativos».


La carga de archivos tiene muchas partes que en realidad están más relacionadas con Django que con el ámbito de Django Ninja, por lo que las repasaré brevemente.

Configuración del proyecto Django#

Antes de implementar la función de carga de archivos, primero debemos decirle a Django cómo manejar los archivos subidos. Esto involucra la configuración de MEDIA_URL y MEDIA_ROOT.

Configuración de MEDIA_URL y MEDIA_ROOT#

  • MEDIA_URL: Este es el prefijo de la URL de los archivos; todos los archivos subidos se accederán a través de esta URL.
  • MEDIA_ROOT: Esta es la ruta interna en el servidor Django donde realmente se almacenan los archivos subidos.

Agrega el siguiente código en el settings.py del proyecto:

# NinjaForum/settings.py
...

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

De esta manera, los archivos subidos se guardarán en la carpeta media ubicada en la raíz del proyecto y se accederán a través de la ruta /media/.

Acceso a archivos en el entorno de desarrollo#

Necesitamos el método static proporcionado por Django para permitir que el entorno de desarrollo acceda directamente a estos archivos.

Agrega esta línea en el urls.py del proyecto:

# NinjaForum/urls.py
from django.conf import settings
from django.conf.urls.static import static
...

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', api.urls),
    # Permite que el entorno de desarrollo acceda a los archivos subidos (solo para uso en desarrollo)
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Este fragmento de código permite a Django servir archivos estáticos en el entorno de desarrollo.

Crear el campo ImageField#

ImageField es un campo que Django utiliza específicamente para almacenar imágenes. En realidad, almacena la ruta del archivo de la imagen. Un campo similar es FileField.

El código es el siguiente:

# user/models.py
class User(AbstractUser):
    ...
    avatar = models.ImageField(upload_to='avatars/', null=True)

Combinado con la configuración previa de settings.py, el campo avatar guardará la imagen subida en la carpeta media/avatars/; esta ruta se determina conjuntamente por MEDIA_ROOT y el argumento upload_to del campo.

Al cambiar a esta rama, el proyecto ya contiene un nuevo archivo de migración, así que recuerda migrar la base de datos:

python manage.py migrate
# o
make migrate

Por cierto, ImageField depende de un paquete de terceros: Pillow. Este paquete proporciona las capacidades de procesamiento de imágenes para el campo; debes instalarlo para que el campo funcione correctamente:

pip install Pillow
# o
poetry add pillow

Los lectores que utilicen Poetry pueden simplemente ejecutar poetry install en la rama después de fusionarla.


Implementación: subir avatar#

Terminados los preparativos, finalmente podemos pasar al plato fuerte.

A continuación se muestra la función completa de «subir avatar»:

from ninja import File, Router, UploadedFile
from ninja.errors import HttpError
...

@router.post('/users/{int:user_id}/avatar/', summary='Subir avatar')
def upload_avatar(
    request: HttpRequest,
    user_id: int,
    avatar_file: UploadedFile = File()
) -> dict[str, str]:
    """
    Subir avatar
    """
    # Verificar el tipo de archivo
    if not avatar_file.content_type.startswith('image/'):
        raise HttpError(400, 'El archivo debe ser un formato de imagen')

    user = User.objects.get(id=user_id)
    user.avatar = avatar_file
    user.save()
    return {'detail': 'Imagen subida exitosamente'}

A continuación se realiza el análisis clave de este código.

1. Definición del parámetro UploadedFile#

En la firma de la función view, UploadedFile se utiliza como type hint, mientras que el parámetro avatar_file representa el archivo subido.

Puedes nombrar libremente el parámetro avatar_file, por ejemplo, en la documentación de ejemplo se llama file. Le he puesto un nombre diferente a propósito para enfatizar que su nombre es completamente personalizable.

¡Pero ojo! Sin importar qué nombre elijas, al enviar la solicitud, la clave en el body también debe utilizar el mismo nombre.

En este punto, la solicitud HTTP debería verse así: (presta atención a avatar_file en el body)

POST /users/1/avatar/
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

# A continuación se muestra el contenido del body
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar_file"; filename="example.jpg"
Content-Type: image/jpeg

(binary image data here)
------WebKitFormBoundary7MA4YWxkTrZu0gW--

El Content-Type en los Headers es multipart/form-data, y cada par clave-valor comienza con Content-Disposition: form-data;. Este formato permite enviar múltiples tipos de datos diferentes simultáneamente en una sola solicitud, incluidos texto y archivos binarios.

Para más detalles, puedes consultar este artículo: «Exploración inicial de multipart/form-data».

2. La función File#

El propósito de definir =File() es decirle a Django Ninja que este parámetro debe obtenerse de la sección de «archivos subidos» en la solicitud HTTP. Un enfoque similar es el de Query mencionado en la entrega 11.

Sin esta marca, el framework podría no ser capaz de identificar y procesar correctamente el contenido subido.

3. Verificar el tipo de archivo#

Utilizamos la propiedad content_type de UploadedFile para obtener el tipo de archivo de este contenido en el body y confirmar que sea una imagen antes de guardarlo.

# Verificar el tipo de archivo
if not avatar_file.content_type.startswith('image/'):
    raise HttpError(400, 'El archivo debe ser un formato de imagen')

Aunque este método es rudimentario, es suficiente para una función simple de carga de imágenes.

Resultado de probar la carga de un archivo de texto plano:

// 400 Bad Request
{
    "detail": "El archivo debe ser un formato de imagen"
}

En entornos de producción, necesitarás verificaciones más estrictas, como usar paquetes especializados en procesamiento de imágenes para validar el contenido del archivo.

4. Guardar la imagen#

Finalmente, asignamos la imagen al campo avatar del modelo User y llamamos al método save().

Django manejará automáticamente el almacenamiento del archivo. Si el nombre está duplicado, generará automáticamente un nombre de archivo único y colocará el archivo en la ubicación que especificamos anteriormente.

Después de subir exactamente el mismo avatar dos veces a través de la API, usemos el comando tree en la raíz del proyecto para ver el resultado:

 tree media
media
└── avatars
    ├── my-avatar.png
    └── my-avatar_gVwgCiG.png  # Segunda carga con el mismo nombre, renombrado automáticamente

2 directories, 2 files

Como se puede observar, la imagen de la segunda carga fue «renombrada automáticamente». Esto garantiza la unicidad del nombre de archivo.

En un entorno de producción, es mejor definir un formato de nomenclatura unificado para tus archivos para garantizar una mejor gestión y seguridad.


Resumen y siguientes pasos#

Este artículo ha presentado cómo implementar la función de carga de archivos en Django Ninja, desde la configuración previa hasta la implementación de la API, detallando el uso de UploadedFile.

A continuación, presentaremos otra función avanzada: la Paginación (Pagination), la cual te ayudará a mejorar eficazmente el rendimiento y la experiencia del usuario al responder con grandes volúmenes de datos.