Carga de archivos

Ah, sí, el viejo problema de la subida de archivos. La idea básica de la subida de archivos es en realidad bastante simple. Básicamente funciona así:

  1. Una etiqueta <form> se marca con enctype=multipart/form-data y se coloca un <input type=file> en ese formulario.

  2. La aplicación accede al archivo desde el diccionario files del objeto de la petición.

  3. utiliza el método save() del archivo para guardar el archivo de forma permanente en algún lugar del sistema de archivos.

Una gentil introducción

Comencemos con una aplicación muy básica que sube un archivo a una carpeta de subida específica y muestra un archivo al usuario. Veamos el código de arranque de nuestra aplicación:

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

Así que primero necesitamos un par de importaciones. La mayoría deberían ser sencillas, la werkzeug.secure_filename() se explica un poco más tarde. El UPLOAD_FOLDER es donde almacenaremos los archivos subidos y el ALLOWED_EXTENSIONS es el conjunto de extensiones de archivo permitidas.

¿Por qué limitamos las extensiones permitidas? Probablemente no quiera que sus usuarios puedan subir todo allí si el servidor está enviando directamente los datos al cliente. De este modo, puede asegurarse de que los usuarios no puedan subir archivos HTML que puedan causar problemas de XSS (véase Secuencia de comandos en sitios cruzados (XSS)). También asegúrese de no permitir archivos .php si el servidor los ejecuta, pero ¿quién tiene PHP instalado en su servidor, verdad? :)

A continuación las funciones que comprueban si una extensión es válida y que suben el archivo y redirigen al usuario a la URL del archivo subido:

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('download_file', name=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

Entonces, ¿qué hace realmente esa función secure_filename()? Ahora el problema es que existe ese principio llamado «nunca confíes en la entrada del usuario». Esto también es cierto para el nombre de un archivo subido. Todos los datos de los formularios enviados pueden ser falsificados, y los nombres de los archivos pueden ser peligrosos. Por el momento sólo recuerda: utiliza siempre esa función para asegurar un nombre de archivo antes de almacenarlo directamente en el sistema de archivos.

Información para los profesionales

¿Así que te interesa saber qué hace esa función secure_filename() y cuál es el problema si no la usas? Pues imagina que alguien envía la siguiente información como filename a tu aplicación:

filename = "../../../../home/username/.bashrc"

Asumiendo que el número de ../ es correcto y que unirías esto con el UPLOAD_FOLDER el usuario podría tener la capacidad de modificar un archivo en el sistema de archivos del servidor que no debería modificar. Esto requiere un poco de conocimiento sobre el aspecto de la aplicación, pero créeme, los hackers son pacientes :)

Ahora veamos cómo funciona esa función:

>>> secure_filename('../../../../home/username/.bashrc')
'home_username_.bashrc'

Queremos ser capaces de servir los archivos subidos para que puedan ser descargados por los usuarios. Definiremos una vista download_file para servir los archivos de la carpeta de subida por su nombre. url_for("download_file", name=name) genera URLs de descarga.

from flask import send_from_directory

@app.route('/uploads/<name>')
def download_file(name):
    return send_from_directory(app.config["UPLOAD_FOLDER"], name)

Si estás utilizando middleware o el servidor HTTP para servir archivos, puedes registrar el endpoint download_file como build_only para que url_for funcione sin una función de vista.

app.add_url_rule(
    "/uploads/<name>", endpoint="download_file", build_only=True
)

Mejora de las cargas

Changelog

Nuevo en la versión 0.6.

¿Cómo gestiona Flask las subidas de archivos? Bueno, los almacenará en la memoria del servidor web si los archivos son razonablemente pequeños, de lo contrario en una ubicación temporal (como se devuelve con tempfile.gettempdir()). ¿Pero cómo se especifica el tamaño máximo de los archivos después del cual se aborta la carga? Por defecto, Flask aceptará sin problemas subidas de archivos con una cantidad ilimitada de memoria, pero se puede limitar estableciendo la clave de configuración MAX_CONTENT_LENGTH:

from flask import Flask, Request

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000

El código anterior limitará la carga útil máxima permitida a 16 megabytes. Si se transmite un archivo mayor, Flask lanzará una excepción RequestEntityTooLarge.

Problema de reinicio de la conexión

Cuando se utiliza el servidor de desarrollo local, es posible que obtenga un error de restablecimiento de la conexión en lugar de una respuesta 413. Obtendrá la respuesta de estado correcta cuando ejecute la aplicación con un servidor WSGI de producción.

Esta característica fue añadida en Flask 0.6, pero también se puede conseguir en versiones anteriores subclasificando el objeto request. Para más información al respecto consulte la documentación de Werkzeug sobre el manejo de archivos.

Cargar barras de progreso

Hace un tiempo muchos desarrolladores tuvieron la idea de leer el archivo entrante en pequeños trozos y almacenar el progreso de subida en la base de datos para poder sondear el progreso con JavaScript desde el cliente. El cliente pregunta al servidor cada 5 segundos cuánto ha transmitido, pero esto es algo que ya debería saber.

Una solución más fácil

Ahora hay mejores soluciones que funcionan más rápido y son más fiables. Hay bibliotecas de JavaScript como jQuery que tienen plugins de formulario para facilitar la construcción de la barra de progreso.

Debido a que el patrón común para la subida de archivos existe casi sin cambios en todas las aplicaciones que se ocupan de las subidas, también hay algunas extensiones de Flask que implementan un mecanismo de subida completo que permite controlar qué extensiones de archivo se pueden subir.