Concluir e cancelar uploads multipart

CloudflareBeginner
Pratique Agora

Introdução

Um uploader de backups deve concluir os uploads bem-sucedidos e liberar as partes abandonadas após uma interrupção. Você dividirá um arquivo sintético limitado, concluirá a sessão multipart e inspecionará e cancelará uma sessão separada que ficou incompleta, sem afetar os objetos já concluídos.

Conclua primeiro as lições anteriores sobre objetos do R2 e credenciais com escopo. Esta VM nova tem Node.js 22.22.0, Wrangler 4.131.1 e AWS SDK 3.888.0. Você criará um bucket Standard privado e temporário, com credenciais próprias de curta duração. O R2 deve estar ativo; consulte os limites de multipart e os preços. As partes incompletas contam para o armazenamento. Este laboratório transfere apenas um pequeno arquivo sintético e não exige um domínio. Não reutilize uploads nem buckets anteriores.

Crie seu bucket privado de documentos

Nesta etapa, você autorizará esta VM e criará um bucket temporário. A autorização do dispositivo confirma sua conta de aprendizado. O gerenciamento de buckets do R2 usa um token de API separado, restrito a essa conta.

Inicie o Bash para usar a sintaxe dos comandos abaixo. Em seguida, acesse o projeto preparado e verifique as ferramentas. Mantenha este mesmo terminal aberto para que as variáveis com os nomes dos recursos continuem disponíveis:

bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version

Autorize o código de dispositivo exibido no seu próprio navegador. Confirme a conta de aprendizado e os escopos de leitura da conta e do usuário solicitados antes de conceder o consentimento:

npx wrangler login --device --browser=false --scopes account:read user:read
npx wrangler whoami --json

Exija loggedIn: true. Leia o nome da conta mesmo que apenas uma conta seja exibida. Substitua YOUR_ACCOUNT_ID abaixo pelo ID real de 32 caracteres dessa conta. openssl rand -hex 6 gera doze caracteres hexadecimais aleatórios, evitando colisões com execuções anteriores deste laboratório. O here-document grava um arquivo de configuração padrão; o shell substitui nele os valores das suas variáveis.

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r06-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON

Para gerenciar o bucket, abra a página API Tokens do seu perfil da Cloudflare e crie um token personalizado com um nome relacionado a este laboratório. Conceda Account → Workers R2 Storage → Edit e restrinja Account Resources à conta de aprendizado cujo ID você salvou. Defina uma expiração curta. Não inclua outras contas nem permissões não relacionadas. Este token de gerenciamento serve para administrar buckets, incluindo sua criação e exclusão. Mais adiante nesta mesma etapa, você criará um token de objetos separado, limitado a este bucket, para as operações do SDK do S3.

Cole o token uma única vez no prompt da VM oculta. umask 077 restringe o arquivo ao seu usuário; read -s oculta o que você digita. O arquivo usa a variável de token padrão do Wrangler e é excluído do Git.

umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN

Use --env-file=.env.management somente nos comandos de gerenciamento do R2; o whoami comum continuará verificando a autorização do dispositivo na VM.

Coloque --env-file no final de cada comando do Wrangler para que a lista de argumentos de arquivos não inclua o nome do comando. Depois de criar cada bucket, se o Wrangler perguntar se deve adicionar uma vinculação à configuração, digite n e pressione Enter. A configuração já contém a vinculação necessária.

npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management

Liste seus buckets e encontre o nome exato gerado. Os outros buckets pertencem a outros trabalhos; não os altere.

npx wrangler r2 bucket list --env-file=.env.management

No Dashboard, abra Storage & databases → R2 → Overview, selecione exatamente este bucket e verifique se a lista de objetos está vazia. Nas configurações dele, mantenha desativados o URL de desenvolvimento público e os domínios personalizados. O nome do bucket no Dashboard confirma a identidade; as verificações de download posteriores comprovarão quais bytes foram armazenados.

A API compatível com S3 permite que SDKs de armazenamento padrão acessem o R2. Ela usa um par de chaves de acesso separado, em vez do token de dispositivo do Wrangler. Em R2 Overview, use Account Details → API Tokens → Manage e crie um User API token com um nome relacionado ao nome do recurso gerado neste laboratório. Escolha Object Read & Write, restrinja o token exatamente a este novo bucket e selecione uma expiração curta, se o formulário oferecer essa opção. Não escolha todos os buckets nem o acesso Admin. Mantenha esta página do token aberta até armazenar o segredo que será exibido uma única vez.

Use os prompts Bash a seguir na VM. read -s oculta o que você digita; umask 077 faz com que o arquivo de credenciais possa ser lido somente pelo seu usuário. Esses nomes são as variáveis de ambiente padrão do AWS SDK. Cole o Access Key ID e o Secret Access Key nos respectivos prompts e pressione Enter. Não cole o valor do token de API geral.

umask 077
read -r -s -p 'Access Key ID: ' AWS_ACCESS_KEY_ID; printf '\n'
read -r -s -p 'Secret Access Key: ' AWS_SECRET_ACCESS_KEY; printf '\n'
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n' "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" > .env.s3
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY

Grave um cliente reutilizável do SDK padrão. O SDK exige uma string de região; o R2 usa auto. Ler a configuração existente mantém as operações da CLI e do SDK direcionadas à mesma conta e ao mesmo bucket.

cat > storage.mjs <<'JS'
import { S3Client } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("wrangler.jsonc", "utf8"));
export const Bucket = config.r2_buckets[0].bucket_name;
export const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${config.account_id}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});
JS

Envie o manual sintético que deve ser preservado usando o Wrangler. Ele deve permanecer inalterado durante as operações de conclusão e cancelamento:

npx wrangler r2 object put "$BUCKET/retained/handbook.txt" --remote --file retained.txt --content-type text/plain --env-file=.env.management

Inicie um upload multipart limitado

Nesta etapa, você iniciará um upload multipart: uma sessão de upload no servidor que aceita partes numeradas antes de montar o objeto final. As partes enviadas ainda não formam um objeto disponível para download. Salvar o ID do upload permite retomar ou cancelar exatamente esta sessão.

Crie um arquivo binário sintético de 6 MiB usando o Python padrão. A primeira parte terá 5 MiB e a parte final terá 1 MiB. O R2 exige tamanhos de partes compatíveis; as partes que não forem a última devem ter pelo menos 5 MiB e usar tamanhos iguais. Esse arquivo pequeno demonstra o protocolo sem exigir uma transferência grande.

python3 - <<'DATA'
from pathlib import Path
Path("archive.bin").write_bytes(bytes(range(256)) * (6 * 1024 * 1024 // 256))
DATA
cat > start.mjs <<'JS'
import { CreateMultipartUploadCommand } from "@aws-sdk/client-s3";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const Key = "exports/archive.bin";
const result = await s3.send(new CreateMultipartUploadCommand({ Bucket, Key, ContentType: "application/octet-stream" }));
writeFileSync("upload.json", JSON.stringify({ Key, UploadId: result.UploadId }));
console.log("Started multipart upload for", Key);
JS
node --env-file=.env.s3 start.mjs

Mantenha upload.json: ele identifica esta operação, mas não é um indicador de sucesso. Não execute o comando de início novamente sem necessidade; cada chamada cria outro upload incompleto que precisará ser removido. Continue usando o ID salvo na criação do upload. No endpoint R2 testado, a lista retornou outra cadeia de ID opaco; compare a chave exata do objeto e use o ID salvo com ListParts para confirmar a sessão ativa.

Envie as partes na ordem e conclua o objeto

Nesta etapa, você enviará as duas partes e informará ao R2 quais identificadores retornados formam o objeto final. A numeração das partes começa em 1. A solicitação de conclusão inclui cada ETag exatamente como foi retornada pelo upload da parte; ela não é igual ao hash que você obteria calculando sozinho o arquivo-fonte completo.

cat > complete.mjs <<'JS'
import { UploadPartCommand, CompleteMultipartUploadCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const { Key, UploadId } = JSON.parse(readFileSync("upload.json", "utf8"));
const bytes = readFileSync("archive.bin");
const size = 5 * 1024 * 1024;
const Parts = [];
for (let offset = 0, PartNumber = 1; offset < bytes.length; offset += size, PartNumber++) {
  const result = await s3.send(new UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body: bytes.subarray(offset, offset + size) }));
  Parts.push({ PartNumber, ETag: result.ETag });
  console.log("Uploaded part", PartNumber);
}
await s3.send(new CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts } }));
const object = await s3.send(new GetObjectCommand({ Bucket, Key }));
writeFileSync("completed.bin", await object.Body.transformToByteArray());
console.log("Completed and downloaded", Key);
JS
node --env-file=.env.s3 complete.mjs

Exija duas linhas indicando as partes enviadas, seguidas pela linha de conclusão. Compare o arquivo baixado byte a byte:

cmp archive.bin completed.bin && printf "Multipart bytes match\n"

A ETag de um objeto multipart não é necessariamente um MD5 do arquivo final. A comparação byte a byte comprova diretamente que o conteúdo foi preservado. No Dashboard, abra o bucket deste laboratório e verifique exports/archive.bin; o manual preservado também deve continuar presente.

Desmarque View prefixes as folders para ver as duas chaves completas como neste exemplo. O nome gerado do seu bucket será diferente. 6.29 MB é a apresentação decimal de 6 MiB (6.291.456 bytes). O resumo superior Bucket Size: 0 B pode demorar a atualizar; confirme o conteúdo pelas linhas dos objetos e pelos bytes verificados pela API.

Objeto multipart concluído e manual preservado

Inspecione um upload incompleto

Nesta etapa, você deixará deliberadamente um novo upload incompleto e listará a sessão e suas partes. As partes incompletas consomem armazenamento, embora uma lista normal de objetos não mostre um arquivo concluído. Por isso, a limpeza precisa de um inventário de uploads além do inventário de objetos.

cat > abandon.mjs <<'JS'
import { CreateMultipartUploadCommand, UploadPartCommand, ListMultipartUploadsCommand, ListPartsCommand } from "@aws-sdk/client-s3";
import { readFileSync, writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const Key = "temporary/unfinished.bin";
const result = await s3.send(new CreateMultipartUploadCommand({ Bucket, Key }));
const UploadId = result.UploadId;
writeFileSync("abandoned.json", JSON.stringify({ Key, UploadId }));
await s3.send(new UploadPartCommand({ Bucket, Key, UploadId, PartNumber: 1, Body: readFileSync("archive.bin").subarray(0, 5 * 1024 * 1024) }));
const uploads = await s3.send(new ListMultipartUploadsCommand({ Bucket }));
console.log(uploads.Uploads.map(upload => ({ key: upload.Key, uploadId: upload.UploadId })));
const parts = await s3.send(new ListPartsCommand({ Bucket, Key, UploadId }));
console.log(parts.Parts.map(part => ({ part: part.PartNumber, bytes: part.Size })));
JS
node --env-file=.env.s3 abandon.mjs

O inventário multipart contém temporary/unfinished.bin. A solicitação ListParts usa o ID salvo e deve retornar a parte 1 com 5,242,880 bytes. Não compare o texto do ID da lista com o ID salvo nem crie outra sessão para repetir uma leitura. Use o ID salvo com as APIs de listagem e execute a verificação enquanto este upload ainda existir.

Cancele somente a sessão abandonada

Nesta etapa, você liberará as partes incompletas cancelando o upload exato por meio do seu ID. Cancelar é diferente de excluir um objeto concluído. A operação deve manter intactos o arquivo compactado concluído e o manual.

cat > abort.mjs <<'JS'
import { AbortMultipartUploadCommand, ListMultipartUploadsCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const { Key, UploadId } = JSON.parse(readFileSync("abandoned.json", "utf8"));
await s3.send(new AbortMultipartUploadCommand({ Bucket, Key, UploadId }));
const uploads = await s3.send(new ListMultipartUploadsCommand({ Bucket }));
console.log("Incomplete uploads:", uploads.Uploads || []);
JS
node --env-file=.env.s3 abort.mjs

Este novo bucket agora deve mostrar uma lista vazia de uploads multipart. A verificação da plataforma também baixa os dois objetos concluídos para comprovar que continuam inalterados. Nunca interprete uma listagem que falhou como uma lista vazia.

Remova os arquivos concluídos e o bucket

Nesta etapa, você removerá exatamente os dois objetos concluídos depois que a verificação do cancelamento for aprovada. A limpeza explícita não espera a regra padrão do ciclo de vida para uploads incompletos.

npx wrangler r2 object delete "$BUCKET/exports/archive.bin" --remote --env-file=.env.management
npx wrangler r2 object delete "$BUCKET/retained/handbook.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
npx wrangler r2 bucket list --env-file=.env.management

Confirme apenas o nome gerado deste bucket. Exija que ele não apareça em um inventário executado com sucesso e execute a verificação de limpeza da plataforma antes de revogar as credenciais.

Revogue a credencial do laboratório e saia

Nesta etapa, você encerrará o acesso deixado por este exercício. Na página R2 API Tokens, revogue somente o token de objetos identificado com o nome deste laboratório. Na página API Tokens do seu perfil, revogue o token de gerenciamento do R2 separado que você criou para este laboratório. Excluir um bucket não revoga um token, e sair do Wrangler não revoga as credenciais do S3.

Depois da revogação, remova o arquivo local de credenciais e faça logout desta VM:

rm .env.s3 .env.management
npx wrangler logout

Inspecione a identidade estruturada. O status diferente de zero é esperado quando você está desconectado:

npx wrangler whoami --json || true

Exija loggedIn: false; mantenha seu login comum no Dashboard. As verificações da plataforma conferem a remoção das credenciais locais e o logout do Wrangler. As duas revogações de token são checkpoints manuais no Dashboard nesta avaliação; elas não são inferidas pela exclusão dos arquivos.

Resumo

Conclua um objeto multipart com bytes exatos, inspecione e cancele partes não finalizadas, preserve outros objetos e remova as credenciais de armazenamento.