Upload de arquivos grandes: multipart e upload retomável

No upload multipart, você divide o arquivo em partes, envia cada uma separadamente e pede ao storage que as junte. Se uma parte falhar, você reenvia só ela, e não o arquivo inteiro.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Entender como o upload multipart divide, envia e junta um arquivo
  • Enviar um arquivo grande e retomar um envio interrompido com Python
  • Evitar os erros comuns: partes pequenas demais, ETag perdido e partes órfãs
Nesta página

Enviar um vídeo de 4 GB em uma única requisição HTTP funciona até o Wi-Fi oscilar aos 97%. Aí o envio cai e você recomeça do zero. Um servidor que recebe esse arquivo também precisa manter uma conexão aberta por muito tempo, com timeouts de proxy e limites de corpo de requisição no caminho.

O upload multipart resolve isso na raiz: o arquivo vira várias partes menores, cada uma com sua própria requisição. Os números abaixo são do Amazon S3, que define o padrão seguido por quase todo storage compatível com S3; confira os limites atuais na documentação oficial.

Iniciarrecebe oupload ID12Enviar partesem paraleloParte 1ETagParte 2Parte 3ETagParte 2reenviadaETagConcluirnúmerose ETags3Objetopartes unidasem ordem
Cada parte vai numa requisição própria; se uma falha, só ela é reenviada, e o storage junta tudo no fim.

Como funciona um upload multipart?#

A documentação do S3 descreve três etapas:

  1. Iniciar (CreateMultipartUpload). O storage devolve um UploadId, que identifica esse envio em todas as chamadas seguintes. Os metadados do objeto vão nesta chamada.
  2. Enviar as partes (UploadPart). Cada parte tem um número de 1 a 10.000. O storage responde com um ETag para cada uma. Você pode enviar em qualquer ordem e em paralelo.
  3. Concluir (CompleteMultipartUpload). Você manda a lista de números de parte e seus ETags. O storage junta as partes na ordem dos números e cria o objeto.

Enviar de novo uma parte com o mesmo número sobrescreve a anterior. É assim que você reenvia uma parte que falhou sem tocar nas outras.

Quais são os limites do multipart no S3?#

Item Limite no Amazon S3
Tamanho máximo do objeto 48,8 TiB
Número de partes 1 a 10.000
Tamanho de cada parte 5 MiB a 5 GiB (a última pode ser menor)

Disso saem duas contas. Com no máximo 10.000 partes, um arquivo de 100 GB exige partes de pelo menos 10 MB. E partes de 5 MiB limitam o arquivo a cerca de 48,8 GiB (10.000 × 5 MiB). Calcule o tamanho da parte como tamanho_do_arquivo / 10.000, arredondado para cima, e nunca abaixo de 5 MiB.

Outros storages compatíveis com S3 podem ter limites diferentes. Confira a documentação do seu fornecedor antes de fixar o tamanho da parte.

Como enviar um arquivo grande em Python?#

O caminho mais curto é o upload_file do boto3, que decide sozinho quando usar multipart e envia as partes em threads. Instale com 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"),  # omita no 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,  # padrão: 8 MiB
    multipart_chunksize=64 * 1024 * 1024,  # padrão: 8 MiB por parte
    max_concurrency=10,                    # padrão: 10 threads
)

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

Os padrões do TransferConfig são 8 MiB para limite e tamanho de parte, e 10 threads. Se a conexão for estável, partes maiores reduzem o número de requisições. Se for instável, partes menores perdem menos a cada falha.

Para verificar, rode aws s3 ls s3://meu-bucket/videos/ --human-readable (com --endpoint-url em outro storage) e compare o tamanho com o do arquivo local.

Como retomar um upload interrompido?#

O upload_file não retoma sozinho: se o processo morre, as partes já enviadas ficam no storage, mas o próximo upload_file começa um envio novo. Para retomar, use as chamadas de baixo nível e grave o upload ID e o ETag de cada parte num arquivo local, à medida que as partes são aceitas.

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 = "meu-bucket", "videos/video.mp4", "video.mp4"
PART = 64 * 1024 * 1024  # 64 MiB: cobre arquivos de até ~625 GiB em 10.000 partes
STATE = PATH + ".upload.json"  # upload ID e ETags, salvos a 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 da parte (texto no 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("concluído")

Se o script cair no meio, rode de novo: ele lê o arquivo de estado, pula as partes já registradas e envia só as que faltam. A documentação do S3 pede para concluir com a sua própria lista de números e ETags; o list_parts serve só para conferir o que chegou ao storage. O PART precisa ser o mesmo nas duas execuções, senão as partes antigas não encaixam. O código lê uma parte por vez, então a memória usada é a de uma parte, não a do arquivo.

O exemplo envia as partes em sequência para o código ficar legível. Para paralelismo, use o upload_file do exemplo anterior ou um ThreadPoolExecutor chamando upload_part.

Como enviar do navegador sem passar pelo seu servidor?#

O padrão é o servidor assinar uma URL assinada por parte, e o navegador enviar cada parte direto ao storage. No servidor, depois do create_multipart_upload:

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

No navegador, fatie o arquivo com file.slice(inicio, fim) e envie cada fatia com fetch(url, { method: "PUT", body: fatia }). A resposta traz o ETag no cabeçalho ETag. Para o JavaScript conseguir lê-lo, a configuração de CORS do bucket precisa expor esse cabeçalho (ExposeHeaders). Ao final, o navegador devolve a lista de ETags ao seu servidor, que chama complete_multipart_upload.

Quando usar o protocolo tus?#

O multipart do S3 retoma porque o storage guarda as partes. O tus é outro caminho: um protocolo aberto sobre HTTP (versão 1.0.0) em que o servidor lembra quantos bytes recebeu. O cliente pergunta com HEAD e continua com 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]

Se o Upload-Offset enviado não bater com o do servidor, ele responde 409 Conflict. Escolha tus quando você já opera um servidor de uploads e quer retomar a partir de qualquer cliente. Para enviar direto ao storage, o multipart com URLs assinadas dispensa esse servidor.

Quais erros evitar?#

  • Partes menores que 5 MiB. No S3, toda parte, exceto a última, precisa ter pelo menos 5 MiB.
  • Perder o ETag ou o número da parte. O complete exige a lista completa. Guarde cada par assim que a parte for aceita.
  • Mudar o tamanho da parte ao retomar. As partes antigas deixam de bater com as novas.
  • Deixar uploads incompletos. No S3, as partes ficam guardadas e cobradas até o upload ser concluído ou abortado. Chame abort_multipart_upload ao desistir e crie uma regra de ciclo de vida com AbortIncompleteMultipartUpload (por exemplo, DaysAfterInitiation: 7). Veja também regras de ciclo de vida.
  • Esperar que o ETag seja o MD5 do arquivo. Em objetos multipart, o ETag não é necessariamente o MD5 dos dados. Para conferir integridade, use os checksums do S3.
  • Usar multipart para arquivos pequenos. Cada parte é uma requisição a mais. Para arquivos pequenos, um PUT simples resolve com uma requisição só.

Como fazer isso na Square Cloud#

No Blob Storage da Square Cloud, o vídeo de 4 GB do início cabe com folga: cada objeto pode ter até 10 GB. O multipart está disponível e só é obrigatório acima de 100 MB por PUT; abaixo disso, um PUT simples resolve. Como a API é compatível com S3, os dois scripts deste artigo rodam sem mudar a lógica: defina S3_ENDPOINT=https://s3-blob.squarecloud.app, AWS_DEFAULT_REGION=auto e as chaves S3 que você gera na aba "Acesso S3" do painel, e troque meu-bucket por public ou private, os buckets fixos da conta em que você grava.

O multipart_threshold de 64 MiB do exemplo já fica abaixo dos 100 MB, então todo arquivo que exige multipart segue por ele, e o script de retomada usa as mesmas chamadas de multipart que o Blob aceita. Os limites de parte da tabela são os do Amazon S3; no Blob, o que muda é o teto de 10 GB do objeto final. Para arquivos de até 100 MB enviados do navegador, uma única URL pré-assinada de PUT basta: ela vale por até 7 dias, e o CORS do Blob aceita PUT de qualquer origem.

Não há taxa de entrada nem de saída, e cada plano inclui uma cota de 5 GB a 250 GB. Gere as chaves no painel e rode o script de retomada com um arquivo seu, interrompendo no meio para ver as partes já enviadas sendo puladas. Conheça o Blob Storage e veja a cota de cada plano em Preços.

Blob Storage

Object storage compatível com S3, sem taxa de saída

Saiba mais

Perguntas frequentes

A partir de que tamanho vale usar upload multipart?

A documentação do Amazon S3 recomenda considerar multipart a partir de 100 MB. O boto3 já passa a usar multipart sozinho acima de 8 MiB por padrão. Em conexões instáveis, vale usar antes disso.

Qual é o tamanho máximo de cada parte no S3?

No Amazon S3, cada parte tem de 5 MiB a 5 GiB, exceto a última, que pode ser menor. Um upload aceita até 10.000 partes e o objeto final pode chegar a 48,8 TiB. Storages compatíveis com S3 podem ter limites próprios.

Multipart e upload retomável são a mesma coisa?

Não. Multipart é como o S3 divide um objeto em partes. Retomável é poder continuar de onde parou, e o multipart permite isso porque as partes já enviadas ficam guardadas. O protocolo tus chega ao mesmo resultado por outro caminho, com HEAD e PATCH.

O que acontece com as partes de um upload que nunca terminou?

No Amazon S3, as partes ficam guardadas e são cobradas até você concluir ou abortar o upload. Configure uma regra de ciclo de vida com AbortIncompleteMultipartUpload para limpá-las.

Próximo artigoExpiração de arquivos: regras de ciclo de vida no object storageUma regra de ciclo de vida apaga arquivos sozinha depois de um prazo. Veja como criar regras de expiração por pasta no S3 com boto3 e como verificar que funcionam.