Subir archivos grandes: carga multiparte y reanudable

En una carga multiparte divides el archivo en partes, envías cada una por separado y le pides al storage que las una. Si una parte falla, reenvías solo esa, no el archivo entero.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Entender cómo la carga multiparte divide, envía y une un archivo
  • Subir un archivo grande y reanudar una carga interrumpida con Python
  • Evitar errores comunes: partes muy pequeñas, ETag perdido y partes huérfanas
En esta página

Enviar un video de 4 GB en una sola petición HTTP funciona hasta que el Wi-Fi falla al 97%. La subida se cae y empiezas desde cero. Además, el servidor que recibe ese archivo tiene que mantener una conexión abierta mucho tiempo, con timeouts de proxy y límites de tamaño del cuerpo de la petición en el camino.

La carga multiparte lo resuelve de raíz: el archivo se convierte en varias partes menores, cada una con su propia petición. Los números de abajo son de Amazon S3, un object storage que marca el estándar que sigue casi todo storage compatible con S3; revisa los límites actuales en la documentación oficial.

Iniciarrecibe elupload ID12Enviar partesen paraleloParte 1ETagParte 2Parte 3ETagParte 2reenviadaETagCompletarnúmerosy ETags3Objetopartes unidasen orden
Cada parte va en su propia petición; si una falla, solo esa se reenvía, y el storage las une al final.

¿Cómo funciona una carga multiparte?#

La documentación de S3 describe tres pasos:

  1. Iniciar (CreateMultipartUpload). El storage devuelve un UploadId, que identifica esa carga en todas las llamadas siguientes. Los metadatos del objeto van en esta llamada.
  2. Enviar las partes (UploadPart). Cada parte tiene un número de 1 a 10.000. El storage responde con un ETag para cada una. Puedes enviarlas en cualquier orden y en paralelo.
  3. Completar (CompleteMultipartUpload). Envías la lista de números de parte con sus ETags. El storage une las partes en el orden de los números y crea el objeto.

Volver a enviar una parte con un número ya usado sobrescribe la anterior. Así se reenvía una parte que falló sin tocar las demás.

¿Cuáles son los límites de multiparte en S3?#

Elemento Límite en Amazon S3
Tamaño máximo del objeto 48,8 TiB
Número de partes 1 a 10.000
Tamaño de cada parte 5 MiB a 5 GiB (la última puede ser menor)

De ahí salen dos cuentas. Con 10.000 partes como máximo, un archivo de 100 GB necesita partes de al menos 10 MB. Y partes de 5 MiB limitan el archivo a unos 48,8 GiB (10.000 × 5 MiB). Calcula el tamaño de la parte como tamaño_del_archivo / 10.000, redondeado hacia arriba, y nunca menos de 5 MiB.

Otros storages compatibles con S3 pueden tener límites distintos. Revisa la documentación de tu proveedor antes de fijar el tamaño de la parte.

¿Cómo subir un archivo grande en Python?#

El camino más corto es upload_file de boto3, que decide solo cuándo usar multiparte y envía las partes en hilos. Instálalo con pip install boto3.

python
import os

import boto3
from boto3.s3.transfer import TransferConfig

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ.get("S3_ENDPOINT"),  # omítelo en AWS S3
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)

config = TransferConfig(
    multipart_threshold=64 * 1024 * 1024,  # predeterminado: 8 MiB
    multipart_chunksize=64 * 1024 * 1024,  # predeterminado: 8 MiB por parte
    max_concurrency=10,                    # predeterminado: 10 hilos
)

s3.upload_file("video.mp4", "mi-bucket", "videos/video.mp4", Config=config)

Los valores predeterminados de TransferConfig son 8 MiB para el umbral y el tamaño de parte, y 10 hilos. Si la conexión es estable, partes mayores significan menos peticiones. Si es inestable, partes menores pierden menos en cada fallo.

Para verificar, ejecuta aws s3 ls s3://mi-bucket/videos/ --human-readable (con --endpoint-url en otro storage) y compara el tamaño con el de tu archivo local.

¿Cómo reanudar una carga interrumpida?#

upload_file no reanuda por sí solo: si el proceso muere, las partes ya enviadas quedan en el storage, pero el siguiente upload_file empieza una carga nueva. Para reanudar, usa las llamadas de bajo nivel y guarda el upload ID y el ETag de cada parte en un archivo local a medida que se aceptan las partes.

python
import json
import os

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ.get("S3_ENDPOINT"),
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)

BUCKET, KEY, PATH = "mi-bucket", "videos/video.mp4", "video.mp4"
PART = 64 * 1024 * 1024  # 64 MiB: cubre archivos de hasta ~625 GiB en 10.000 partes
STATE = PATH + ".upload.json"  # upload ID y ETags, guardados tras cada parte

if os.path.exists(STATE):
    with open(STATE) as f:
        state = json.load(f)
else:
    upload_id = s3.create_multipart_upload(Bucket=BUCKET, Key=KEY)["UploadId"]
    state = {"upload_id": upload_id, "parts": {}}


def save():
    with open(STATE, "w") as f:
        json.dump(state, f)


save()
done = state["parts"]  # número de parte (texto en el JSON) -> ETag

with open(PATH, "rb") as f:
    number = 1
    while chunk := f.read(PART):
        if str(number) not in done:
            r = s3.upload_part(
                Bucket=BUCKET, Key=KEY, UploadId=state["upload_id"],
                PartNumber=number, Body=chunk,
            )
            done[str(number)] = r["ETag"]
            save()
            print("parte", number, "enviada")
        number += 1

s3.complete_multipart_upload(
    Bucket=BUCKET, Key=KEY, UploadId=state["upload_id"],
    MultipartUpload={
        "Parts": [
            {"PartNumber": int(n), "ETag": e}
            for n, e in sorted(done.items(), key=lambda item: int(item[0]))
        ]
    },
)
os.remove(STATE)
print("completado")

Si el script se cae a la mitad, ejecútalo de nuevo: lee el archivo de estado, se salta las partes ya registradas y envía solo las que faltan. La documentación de S3 pide completar con tu propia lista de números y ETags; list_parts sirve solo para comprobar qué llegó al storage. PART debe ser el mismo en ambas ejecuciones, o las partes antiguas no encajarán. El código lee una parte a la vez, así que la memoria usada es la de una parte, no la del archivo.

El ejemplo envía las partes en secuencia para que el código sea fácil de leer. Para paralelismo, usa el upload_file del ejemplo anterior o un ThreadPoolExecutor que llame a upload_part.

¿Cómo subir desde el navegador sin pasar por tu servidor?#

El patrón habitual es que tu servidor firme una URL prefirmada por parte y que el navegador envíe cada parte directo al storage. En el servidor, después de create_multipart_upload:

python
url = s3.generate_presigned_url(
    "upload_part",
    Params={"Bucket": BUCKET, "Key": KEY, "UploadId": upload_id, "PartNumber": 1},
    ExpiresIn=3600,
)

En el navegador, corta el archivo con file.slice(inicio, fin) y envía cada trozo con fetch(url, { method: "PUT", body: trozo }). La respuesta trae el ETag en la cabecera ETag. Para que JavaScript pueda leerla, la configuración de CORS del bucket debe exponer esa cabecera (ExposeHeaders). Al final, el navegador devuelve la lista de ETags a tu servidor, que llama a complete_multipart_upload.

¿Cuándo usar el protocolo tus?#

La carga multiparte de S3 se puede reanudar porque el storage guarda las partes. tus es otro camino: un protocolo abierto sobre HTTP (versión 1.0.0) en el que el servidor recuerda cuántos bytes recibió. El cliente pregunta con HEAD y continúa con PATCH:

http
HEAD /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Tus-Resumable: 1.0.0

HTTP/1.1 200 OK
Upload-Offset: 70
Upload-Length: 100
Cache-Control: no-store
Tus-Resumable: 1.0.0
http
PATCH /files/24e533e02ec3bc40c387f1a0e460e216 HTTP/1.1
Tus-Resumable: 1.0.0
Upload-Offset: 70
Content-Type: application/offset+octet-stream
Content-Length: 30

[30 bytes restantes]

Si el Upload-Offset enviado no coincide con el del servidor, este responde 409 Conflict. Elige tus cuando ya operas un servidor de cargas y quieres reanudar desde cualquier cliente. Para enviar directo al storage, la carga multiparte con URLs prefirmadas evita ese servidor.

¿Qué errores evitar?#

  • Partes menores de 5 MiB. En S3, toda parte excepto la última debe tener al menos 5 MiB.
  • Perder el ETag o el número de parte. complete exige la lista completa. Guarda cada par en cuanto la parte sea aceptada.
  • Cambiar el tamaño de la parte al reanudar. Las partes antiguas dejan de encajar con las nuevas.
  • Dejar cargas incompletas. En S3, las partes quedan guardadas y se cobran hasta que la carga se complete o se cancele. Llama a abort_multipart_upload al desistir y crea una regla de ciclo de vida con AbortIncompleteMultipartUpload (por ejemplo, DaysAfterInitiation: 7). Mira también reglas de ciclo de vida.
  • Esperar que el ETag sea el MD5 del archivo. En objetos multiparte, el ETag no es necesariamente el MD5 de los datos. Para comprobar la integridad, usa los checksums de S3.
  • Usar multiparte en archivos pequeños. Cada parte es una petición más. Para archivos pequeños, un PUT simple lo resuelve con una sola petición.

Cómo hacerlo en Square Cloud#

En el Blob Storage de Square Cloud, el video de 4 GB del inicio cabe de sobra: cada objeto puede pesar hasta 10 GB. La carga multiparte está disponible y solo es obligatoria por encima de 100 MB por PUT; por debajo, un PUT simple basta. Como la API es compatible con S3, los dos scripts de este artículo funcionan sin cambiar la lógica: define S3_ENDPOINT=https://s3-blob.squarecloud.app, AWS_DEFAULT_REGION=auto y las claves S3 que generas en la pestaña "Acceso S3" del panel, y cambia mi-bucket por public o private, los buckets fijos de la cuenta en los que escribes.

El multipart_threshold de 64 MiB del ejemplo ya queda por debajo de los 100 MB, así que todo archivo que exige multiparte pasa por ella, y el script de reanudación usa las mismas llamadas de multiparte que acepta el Blob. Los límites de parte de la tabla son los de Amazon S3; en el Blob, lo que cambia es el tope de 10 GB del objeto final. Para archivos de hasta 100 MB enviados desde el navegador, basta una sola URL prefirmada de PUT: vale hasta 7 días, y el CORS del Blob acepta PUT desde cualquier origen.

No hay tarifa de entrada ni de salida, y cada plan incluye una cuota de 5 GB a 250 GB. Genera las claves en el panel y ejecuta el script de reanudación con un archivo tuyo, interrumpiéndolo a la mitad para ver cómo salta las partes ya enviadas. Conoce el Blob Storage y revisa la cuota de cada plan en Precios.

Blob Storage

Object storage compatible con S3, sin tarifas de salida

Más información

Preguntas frecuentes

¿Desde qué tamaño conviene usar carga multiparte?

La documentación de Amazon S3 recomienda considerar multiparte desde los 100 MB. boto3 ya usa multiparte por sí solo por encima de 8 MiB de forma predeterminada. En conexiones inestables conviene usarla antes.

¿Cuál es el tamaño máximo de cada parte en S3?

En Amazon S3 cada parte mide de 5 MiB a 5 GiB, salvo la última, que puede ser menor. Una carga admite hasta 10.000 partes y el objeto final puede llegar a 48,8 TiB. Los storages compatibles con S3 pueden tener límites propios.

¿Multiparte y carga reanudable son lo mismo?

No. Multiparte es la forma en que S3 divide un objeto en partes. Reanudable es poder continuar donde te quedaste, y multiparte lo permite porque las partes ya enviadas quedan guardadas. El protocolo tus llega al mismo resultado por otro camino, con HEAD y PATCH.

¿Qué pasa con las partes de una carga que nunca terminó?

En Amazon S3 las partes quedan guardadas y se cobran hasta que completes o canceles la carga. Configura una regla de ciclo de vida con AbortIncompleteMultipartUpload para limpiarlas.

Siguiente artículoReglas de ciclo de vida: expirar y limpiar archivos automáticamenteUna regla de ciclo de vida borra archivos sola después de un plazo. Aprende a crear reglas de caducidad por carpeta en S3 con boto3 y a comprobar que funcionan.

Más en Storage

Ver los 15 artículos