Subidas de archivos eficientes en Flask: guía paso a paso
Las subidas de archivos son una función crucial en las aplicaciones web modernas, ya que permiten a los usuarios compartir y almacenar datos de forma eficiente. Flask, un framework web de Python ligero pero potente, ofrece capacidades robustas para gestionar las subidas de archivos de forma segura y eficiente. En esta guía paso a paso, exploraremos cómo implementar subidas de archivos en aplicaciones Flask, abarcando buenas prácticas, medidas de seguridad y técnicas avanzadas.
Configurar tu entorno de Flask
Para empezar con las subidas de archivos en Flask, configuraremos una aplicación Flask básica con las dependencias necesarias. Primero, instala las dependencias del sistema para python-magic:
# For Debian/Ubuntu
sudo apt-get install libmagic1
# For macOS
brew install libmagic
# For Windows
# Install libmagic DLLs separately as described in python-magic's Windows instructions.
Ahora instala Flask y sus dependencias con pip:
pip install flask==3.1.0 flask-wtf==1.2.2 python-magic==0.4.27
export FLASK_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
En Windows, sigue las instrucciones de instalación de python-magic
para obtener binarios de libmagic compatibles; no se instalan automáticamente. Define FLASK_SECRET_KEY
en tu shell o en la configuración de secretos de tu despliegue, y mantenlo estable en todos los
workers de la aplicación.
Vamos a crear una estructura básica de aplicación Flask en un archivo llamado app.py:
from flask import Flask
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MiB total request limit
# Ensure the upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], mode=0o700, exist_ok=True)
Configuración explicada:
SECRET_KEY: Una clave secreta que Flask-WTF usa para firmar de forma segura la cookie de sesión y para otras necesidades relacionadas con la seguridad.UPLOAD_FOLDER: Almacenamiento privado fuera del directorio estático de Flask. No expongas el directorio de instancia a través de tu servidor web.MAX_CONTENT_LENGTH: El tamaño total máximo de la solicitud (16 MiB), incluidos los campos multipart y la sobrecarga. El tamaño de archivo permitido es ligeramente menor.
Crear el formulario de subida de archivos
Para facilitar las subidas de archivos, crearemos un formulario de Flask-WTF que incluye validación para los archivos subidos.
Primero, crea una clase de formulario en forms.py:
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
class UploadForm(FlaskForm):
file = FileField('File', validators=[
FileRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'pdf'], 'Allowed file types are jpg, jpeg, png, pdf')
])
Este formulario usa FileField para la entrada de archivos e incluye validadores que garantizan que
se proporcione un archivo y que tenga una extensión permitida.
A continuación, crea una plantilla templates/upload.html para el formulario de subida:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Upload File</title>
</head>
<body>
<h1>Upload File</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }} {{ form.file.label }} {{ form.file }}
{% for errors in form.errors.values() %}
{% for error in errors %}<p role="alert">{{ error }}</p>{% endfor %}
{% endfor %}
<input type="submit" value="Upload" />
</form>
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
<ul>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %} {% endwith %}
</body>
</html>
Esta plantilla renderiza el formulario e incluye una sección para mostrar los mensajes flash que sirven de retroalimentación al usuario.
Implementar el manejo de subidas de archivos
Ahora, gestionemos las subidas de archivos en nuestra aplicación Flask con una validación robusta
del tipo MIME, comprobaciones de la extensión del archivo y manejo de errores. Actualiza tu app.py
para incluir las importaciones y rutas necesarias:
from flask import render_template, redirect, url_for, flash, request, jsonify
from werkzeug.exceptions import HTTPException, RequestEntityTooLarge
from forms import UploadForm
import magic
import tempfile
ALLOWED_TYPES = {
'image/jpeg': ('jpg', 'jpeg'),
'image/png': ('png',),
'application/pdf': ('pdf',),
}
def save_upload(file):
if file is None or not file.filename:
raise ValueError('No file selected')
extension = file.filename.rsplit('.', 1)[-1].lower()
file_type = magic.from_buffer(file.read(2048), mime=True)
file.seek(0)
extensions = ALLOWED_TYPES.get(file_type)
if extensions is None or extension not in extensions:
raise ValueError('File type or extension not allowed')
# Exclusive temporary-file creation gives unique names and mode 0600.
with tempfile.NamedTemporaryFile(
dir=app.config['UPLOAD_FOLDER'], prefix='upload-',
suffix='.' + extensions[0], delete=False,
) as target:
try:
file.save(target)
except Exception:
os.unlink(target.name)
raise
return os.path.basename(target.name)
# Error handler for file size limit
@app.errorhandler(RequestEntityTooLarge)
def handle_file_too_large(e):
return jsonify({'error': 'The upload request exceeds 16 MiB.'}), 413
@app.route('/', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if form.validate_on_submit():
try:
save_upload(form.file.data)
flash('File uploaded successfully', 'success')
except ValueError as e:
flash(str(e), 'danger')
except Exception as e:
app.logger.error('Upload failed (%s)', type(e).__name__)
flash('The file could not be saved. Please try again.', 'danger')
return redirect(url_for('upload'))
return render_template('upload.html', form=form)
Funciones de seguridad:
- El sniffing de MIME comprueba la firma de un archivo, pero no demuestra que el archivo sea inofensivo. Analiza o procesa el contenido no confiable de forma adecuada antes de ponerlo a disposición de otros usuarios.
- La función auxiliar comprueba que la extensión coincida con el tipo detectado.
- Los nombres y las extensiones generados por el servidor impiden que los nombres de archivo del usuario controlen las rutas locales.
- Aplicar un límite de tamaño de archivo protege contra el agotamiento de recursos.
- Los errores devueltos al cliente se sanean; los diagnósticos del servidor registran solo el tipo de excepción.
Crear un endpoint de REST API
Para subidas de archivos programáticas, implementemos un endpoint de REST API:
@app.route('/api/upload', methods=['POST'])
def api_upload():
try:
filename = save_upload(request.files.get('file'))
return jsonify({
'message': 'File uploaded successfully',
'filename': filename
}), 200
except ValueError as e:
return jsonify({'error': str(e)}), 400
Probar el endpoint de la API:
Después de añadir las rutas y el manejador de errores a app.py, inicia Flask y luego usa cURL
desde otra terminal para probar el endpoint de la API:
flask --app app run
curl -F 'file=@/path/to/your/file.jpg' http://localhost:5000/api/upload
Manejar subidas de archivos grandes
Para gestionar de forma eficiente las subidas de archivos grandes, considera estas estrategias:
Configurar nginx para subidas grandes
Si usas Nginx como proxy inverso, añade estos ajustes a tu configuración:
http {
client_max_body_size 16M;
proxy_read_timeout 600;
proxy_connect_timeout 600;
proxy_send_timeout 600;
}
Implementar subidas por fragmentos
Para subidas de archivos grandes, considera un enfoque de subida por fragmentos. El protocolo tus es una excelente opción para este propósito, ya que ofrece capacidades de subida reanudable.
Usar procesamiento asíncrono
Celery traslada el procesamiento a un worker después de que la subida HTTP se guarda; no elimina el límite de tamaño de la solicitud. Este ejemplo opcional de worker está dirigido a Linux/macOS. Instala Celery con su transporte de Redis, instala Redis, y ejecuta un broker local de Redis:
pip install 'celery[redis]==5.6.3'
redis-server
Añade este código a app.py. El ejemplo calcula un resumen SHA-256 en un worker:
from celery import Celery
import hashlib
celery = Celery('tasks', broker='redis://localhost:6379/0')
@celery.task
def process_uploaded_file(file_path):
digest = hashlib.sha256()
with open(file_path, 'rb') as source:
for chunk in iter(lambda: source.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
@app.route('/upload-large', methods=['POST'])
def upload_large_file():
try:
filename = save_upload(request.files.get('file'))
except ValueError as e:
return jsonify({'error': str(e)}), 400
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# Queue the file for processing
task = process_uploaded_file.delay(file_path)
return jsonify({'message': 'File uploaded and queued for processing', 'task_id': task.id}), 202
Ejecuta estos comandos en terminales separadas desde el directorio del proyecto, con FLASK_SECRET_KEY definido en ambas:
celery -A app:celery worker --loglevel=INFO
flask --app app run
El proceso web y el worker deben compartir el mismo directorio privado de subidas. Este ejemplo no tiene backend de resultados ni endpoint de estado. Un fallo del broker devuelve un error saneado; el archivo guardado permanece para que el operador lo limpie. Mantén Redis en privado. Los ejemplos de API demuestran la validación, así que añade la autenticación, la autorización y los límites de tasa de tu aplicación antes de exponerlos públicamente.
Manejo de errores y validación
Implementa un manejo de errores completo para mejorar la fiabilidad:
@app.errorhandler(Exception)
def handle_unexpected_error(error):
if isinstance(error, HTTPException):
return error
app.logger.error('Request failed (%s)', type(error).__name__)
return jsonify({'error': 'An unexpected error occurred'}), 500
Conclusión
Implementar subidas de archivos seguras y eficientes en Flask requiere prestar mucha atención a la seguridad, el rendimiento y la experiencia de usuario. Si sigues las buenas prácticas e incorporas una validación y un manejo de errores robustos, podrás construir un sistema de subida de archivos fiable para tus aplicaciones Flask.
Para funciones de subida de archivos más avanzadas, considera usar herramientas de código abierto como el protocolo tus o Uppy, que se pueden integrar con Flask para admitir características como las subidas reanudables y el seguimiento del progreso. Además, Transloadit ofrece servicios completos de subida y procesamiento de archivos.
