Módulo 4 · Lección 29 de 45
Subida de ficheros
Dos atributos que hay que poner o no llega nada, los ficheros huérfanos que nadie borra, y por qué un fichero subido es siempre sospechoso.
La lección 20 separó dos cosas que se confunden: los estáticos, que escribes tú y viajan con el código, y los ficheros subidos, que aparecen mientras el sitio funciona. Aquellos ya están; estos son los de ahora.
Subir un fichero se parece a cualquier otro campo y tiene cuatro diferencias que muerden. Los datos no llegan por donde tú crees, hay dos atributos sin los cuales no pasa nada y no salta ningún error, los ficheros no se borran cuando se borra la fila, y un fichero es lo más peligroso que se puede aceptar de alguien desconocido.
Los ajustes#
No vienen en settings.py, como se midió en la lección 20:
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'subidos'Los mismos dos conceptos que en los estáticos: MEDIA_URL es el prefijo de las direcciones y MEDIA_ROOT la carpeta del disco. La diferencia es que aquí los dos hacen falta desde el primer día, porque sin MEDIA_ROOT no hay dónde guardar.
Y como con los estáticos, runserver no los sirve por su cuenta. Hay que decírselo, solo en desarrollo:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)/media/fotos/2026/09/mi_foto.png -> 200
/media/no-existe.png -> 404Ese if settings.DEBUG no es una precaución de estilo: esa forma de servir ficheros es insegura y lenta, y por eso Django la desactiva sola en producción. Allí lo hace el servidor de delante, igual que con los estáticos.
Los campos#
foto = models.ImageField('foto', upload_to='fotos/%Y/%m/', blank=True)
adjunto = models.FileField('adjunto', upload_to='adjuntos/', blank=True)upload_to es la subcarpeta dentro de MEDIA_ROOT, y admite trozos de fecha que se sustituyen al guardar. Repartir por año y mes no es cosmético: una carpeta con cincuenta mil ficheros es lenta de listar en casi cualquier sistema.
Cuando la ruta depende del objeto, se pone una función:
def ruta_foto(instancia, nombre_original):
return f'fotos/{instancia.pk}/{nombre_original}'ImageField hace dos cosas de más y necesita Pillow instalado, que es la segunda dependencia del curso:
pip install Pillowfoto <input type="file" name="foto" accept="image/*" id="id_foto">
adjunto <input type="file" name="adjunto" id="id_adjunto">La primera es ese accept, que hace que el navegador filtre. La segunda es que comprueba que el fichero es de verdad una imagen:
un fichero llamado truco.png con el texto "no soy una imagen" dentro:
válido: False
{'foto': ['Envíe una imagen válida. El fichero que ha enviado no era una imagen
o se trataba de una imagen corrupta.']}Fíjate en que lo ha pillado pese al nombre y al tipo declarado. Eso es porque abre el fichero y lo mira, en lugar de fiarse de lo que dice. Es la única validación de contenido que Django trae de serie.
Los dos atributos que hay que poner#
Aquí está el fallo que se comete una vez y cuesta media tarde.
<form method="post" enctype="multipart/form-data">formulario = ContactoForm(peticion.POST, peticion.FILES)Sin el enctype, el navegador manda solo los nombres de los ficheros. Sin peticion.FILES, el formulario no mira dónde están.
Y lo peor es cómo falla:
válido sin FILES: True | foto en cleaned_data: NoneNo hay ningún error. El formulario se valida, se guarda, redirige, y el fichero no está. Con un campo blank=True, que es lo normal en una foto, no se queja nadie.
La regla, y conviene escribirla junto al {% csrf_token %} en la cabeza: si el formulario tiene un fichero, lleva enctype en la plantilla y peticion.FILES en la vista. Las dos cosas, siempre juntas.
En las vistas genéricas de la lección 24 esto ya está hecho: CreateView y UpdateView pasan request.FILES solas. El enctype de la plantilla sigue siendo cosa tuya.
Qué tienes después de guardar#
name : fotos/2026/09/mi_foto.png
url : /media/fotos/2026/09/mi_foto.png
path : /tmp/m2/subidos/fotos/2026/09/mi_foto.png
size : 122
dims : 40 x 30name es la ruta relativa, que es lo único que se guarda en la base de datos: la columna es un texto, no el fichero. url es lo que va en la plantilla, path la ruta del disco, y width y height solo existen en ImageField.
En la plantilla, con la comprobación delante, porque el campo puede estar vacío:
{% if contacto.foto %}
<img src="{{ contacto.foto.url }}" alt="{{ contacto.nombre }}" width="200">
{% endif %}Sin ese {% if %}, un contacto sin foto genera un <img src="">, que el navegador intenta cargar como la propia página.
Los nombres#
Django no deja los nombres tal cual, y lo que hace conviene conocerlo:
'mi foto.png' -> fotos/2026/09/mi_foto.png
el mismo otra vez -> fotos/2026/09/mi_foto_fxRQR34.png
y otra vez -> fotos/2026/09/mi_foto_OIng1bW.pngLos espacios pasan a guiones bajos, y los nombres repetidos no se pisan: se les añade un sufijo aleatorio. Es una decisión sensata, porque lo contrario sería que la foto de alguien sustituyera la de otro por llamarse foto.png.
Y lo importante:
'../../../etc/passwd.png' -> fotos/2026/09/passwd.pngLa travesía de rutas está neutralizada: Django se queda solo con la última parte del nombre. No hay que hacer nada, y hay que saberlo para no fiarse en otros sitios: si tú construyes una ruta con algo que venga de fuera, esa protección no está.
Validar#
ImageField comprueba que es una imagen y nada más. El tamaño y la extensión son cosa tuya:
from django.core.validators import FileExtensionValidator
class SubirForm(forms.Form):
documento = forms.FileField(validators=[FileExtensionValidator(['pdf', 'txt'])])
def clean_documento(self):
fichero = self.cleaned_data['documento']
if fichero.size > 5 * 1024 * 1024:
raise forms.ValidationError('El fichero no puede pasar de 5 MB.')
return ficheroextensión mala -> False La extensión de archivo "exe" no esta permitida. Las permitidas son: pdf, txt.
demasiado grande -> False El fichero no puede pasar de 5 MB.
correcto -> TrueY fíjate en el return fichero del final: es la trampa de la lección 27, y aquí destruiría el fichero entero.
Los límites que ya vienen puestos, medidos:
FILE_UPLOAD_MAX_MEMORY_SIZE: 2 MB
DATA_UPLOAD_MAX_MEMORY_SIZE: 2 MB
DATA_UPLOAD_MAX_NUMBER_FILES: 100
FILE_UPLOAD_PERMISSIONS: 0o644Los dos primeros no son lo mismo, y confundirlos hace perder tiempo. FILE_UPLOAD_MAX_MEMORY_SIZE no es un límite: es el punto a partir del cual Django deja de tener el fichero en memoria y lo escribe en un temporal. Se puede subir un vídeo de un giga con ese valor. DATA_UPLOAD_MAX_MEMORY_SIZE sí es un límite, pero solo del resto del formulario, sin contar los ficheros.
O sea: de serie no hay ningún límite al tamaño de un fichero subido. Si no lo pones tú, alguien puede llenarte el disco. Y conviene ponerlo también en el servidor de delante, porque validar en Django significa que el fichero ya ha viajado entero.