Introdução
Um visualizador de documentos geralmente precisa apenas dos próximos bytes ou da confirmação de que sua cópia em cache ainda está atualizada. Baixar o arquivo inteiro a cada solicitação desperdiça trabalho. Você adicionará validadores HTTP e downloads de intervalos únicos de bytes a um Worker protegido, respaldado por armazenamento privado do R2.
Conclua primeiro o laboratório Stream Documents Through a Worker. Este laboratório começa em uma nova VM com Node.js 22.22.0, Wrangler 4.131.1 e um módulo fornecido para verificar tokens; você criará um novo bucket e implantará um novo Worker. Sua assinatura do R2 e as permissões da sua conta de aprendizado já devem estar prontas. Consulte os preços do R2 para conhecer os custos de operações e armazenamento. Nenhum domínio personalizado é necessário. Apenas texto sintético será armazenado; faça a limpeza antes de sair.
Conectar o bucket da aplicação
Nesta etapa, você autorizará esta VM e criará um bucket privado independente para a aplicação. 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 workers_scripts:write workers_kv:write
npx wrangler whoami --json
Exija loggedIn: true. Leia o nome da conta mesmo que apenas uma conta seja listada. Substitua YOUR_ACCOUNT_ID abaixo pelo ID real de 32 caracteres dessa conta. openssl rand -hex 6 gera doze caracteres hexadecimais aleatórios para evitar que este laboratório colida com uma execução anterior. 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-r03-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"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 o nome deste 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. Neste laboratório, o Worker acessa os objetos do R2 por meio do binding DOCUMENTS.
Copie o token uma única vez para este prompt oculto da VM. umask 077 restringe o acesso do arquivo ao seu usuário; read -s oculta a entrada. 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 normal continua verificando a autorização do dispositivo da 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 gerado exato. 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 esse bucket e verifique se a lista de objetos está vazia. Nas configurações dele, mantenha desativados o URL público de desenvolvimento e os domínios personalizados. O nome do bucket no Dashboard confirma a identidade; as verificações de download posteriores comprovarão os bytes armazenados.
A permissão para scripts do Worker permite a implantação. A permissão do KV permite que o Wrangler mantenha o controle das exclusões; este laboratório não cria nenhum namespace do KV. O token de gerenciamento do R2 continua sendo uma credencial separada, limitada à conta.
Implementar leituras condicionais e parciais
Nesta etapa, você usará os metadados do R2 para decidir se o corpo é necessário. Um ETag funciona como um rótulo de versão do arquivo. Quando o cliente já tem uma cópia, ele envia o rótulo em If-None-Match para perguntar se o arquivo foi alterado. Quando há correspondência, a resposta é 304 Not Modified, sem corpo, evitando outro download dos mesmos bytes. Uma solicitação Range permite que um visualizador busque uma parte de um arquivo grande ou retome um download interrompido. Ela solicita posições de bytes inclusivas e produz 206 Partial Content, incluindo um cabeçalho Content-Range que descreve o trecho.
Use este handler. head() lê os metadados sem ler os bytes. O get() posterior inclui onlyIf.etagMatches, para que um objeto alterado entre essas chamadas não seja retornado usando metadados desatualizados. Este endpoint aceita um único intervalo e If-Range baseado em ETag; uma sintaxe com vários intervalos retorna 400. Quando um ETag em If-Range é diferente, uma resposta completa 200 permite que o cliente substitua sua cópia antiga.
cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path === "/health") return new Response("ok");
if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
if (path !== "/documents/report.txt") return new Response("Not found", { status: 404 });
const key = path.slice(1);
const metadata = await env.DOCUMENTS.head(key);
if (!metadata) return new Response("Not found", { status: 404 });
const headers = new Headers({ "ETag": metadata.httpEtag,
"Last-Modified": metadata.uploaded.toUTCString(), "Accept-Ranges": "bytes",
"Cache-Control": "private, no-store" });
metadata.writeHttpMetadata(headers);
// GET validators use weak comparison: W/"value" and "value" can match.
const noneMatch = request.headers.get("If-None-Match");
if (noneMatch && noneMatch.split(",").some(tag => tag.trim() === "*" || tag.trim().replace(/^W\//, "") === metadata.httpEtag))
return new Response(null, { status: 304, headers });
const since = Date.parse(request.headers.get("If-Modified-Since") || "");
const uploadedSeconds = Math.floor(metadata.uploaded.getTime() / 1000) * 1000;
if (!noneMatch && Number.isFinite(since) && uploadedSeconds <= since)
return new Response(null, { status: 304, headers });
let range = request.headers.get("Range");
const ifRange = request.headers.get("If-Range");
if (ifRange && ifRange !== metadata.httpEtag) range = null;
let start = 0, end = metadata.size - 1;
if (range) {
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
// This endpoint supports exactly one range, not multipart ranges.
if (!match || (!match[1] && !match[2]))
return new Response("Invalid range", { status: 400 });
if (!match[1]) { start = Math.max(0, metadata.size - Number(match[2])); }
else { start = Number(match[1]); if (match[2]) end = Math.min(Number(match[2]), end); }
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= metadata.size) {
headers.set("Content-Range", `bytes */${metadata.size}`);
return new Response("Range not satisfiable", { status: 416, headers });
}
headers.set("Content-Range", `bytes ${start}-${end}/${metadata.size}`);
}
// Do not mix a HEAD result with bytes from an object replaced in between.
const object = await env.DOCUMENTS.get(key, { onlyIf: { etagMatches: metadata.etag },
...(range ? { range: { offset: start, length: end - start + 1 } } : {}) });
if (!object) return new Response("Not found", { status: 404 });
if (!("body" in object)) return new Response("Object changed; retry", { status: 412 });
headers.set("Content-Length", String(range ? end - start + 1 : metadata.size));
return new Response(object.body, { status: range ? 206 : 200, headers });
}
};
JS
A posição inicial solicitada é baseada em zero. Um sufixo como bytes=-3 significa os três últimos bytes. Uma posição inicial além do objeto produz 416 com Content-Range: bytes */SIZE. A validação condicional tem prioridade sobre a seleção do intervalo. If-None-Match tem prioridade sobre a validação por data quando ambos estão presentes.
Crie o segredo local da aplicação e verifique o bundle:
umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars
npx wrangler deploy --dry-run
Comparar corpos locais completos e parciais
Nesta etapa, você preencherá apenas o armazenamento local e examinará cabeçalhos HTTP reais. Um objeto local é separado do objeto remoto posterior, embora ambos usem a mesma chave.
npx wrangler r2 object put "$BUCKET/documents/report.txt" --local --file document.txt --content-type text/plain
npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!
Aguarde a mensagem de pronto em dev.log e carregue o segredo sintético da aplicação:
cat dev.log
set -a
source .dev.vars
set +a
Salve separadamente os cabeçalhos e o corpo da resposta completa. -D grava os cabeçalhos em um arquivo:
curl -fsS -D full.headers -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o full.txt
cmp document.txt full.txt
cat full.headers
Exija 200, o tipo de conteúdo armazenado, um ETag entre aspas e Accept-Ranges: bytes. Copie o ETag exato, incluindo as aspas duplas, para ETAG dentro das aspas simples mostradas abaixo:
ETAG='"COPY_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" http://127.0.0.1:8787/documents/report.txt
Exija 304 sem corpo. Um validador atualizado evita uma transferência completa; ele não torna o bucket público.
curl -sS -D range.headers -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" http://127.0.0.1:8787/documents/report.txt -o range.txt
head -c 5 document.txt > expected-range.txt
cmp expected-range.txt range.txt
cat range.headers
Exija 206, Content-Range: bytes 0-4/SIZE e exatamente cinco bytes correspondentes. Agora solicite uma posição inicial não atendível:
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" http://127.0.0.1:8787/documents/report.txt
Exija 416, o cabeçalho bytes */SIZE e Range not satisfiable. A verificação da plataforma repete essas leituras de forma independente.
Verificar a entrega condicional remota
Nesta etapa, você provisionará o fixture remoto de forma independente e publicará o handler. Pare o servidor local e carregue o mesmo arquivo sintético usando a flag explícita --remote:
kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler r2 object put "$BUCKET/documents/report.txt" --remote --file document.txt --content-type text/plain --env-file=.env.management
npx wrangler deploy
npx wrangler secret bulk .dev.vars
Copie o URL implantado para BASE_URL. Aguarde o health check retornar ok; repita as leituras por até um minuto se a nova implantação ainda estiver sendo propagada.
BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"
curl -fsS -D remote.headers -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote.txt
cmp document.txt remote.txt
cat remote.headers
Use o ETag remoto de remote.headers, não um valor local memorizado. Repita as solicitações condicionais e parciais:
ETAG='"COPY_REMOTE_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" "$BASE_URL/documents/report.txt"
Exija 304 sem corpo, 206 com os cinco primeiros bytes do fixture e 416 com o limite de tamanho correto. No Dashboard, verifique o binding exato do Worker e o objeto do bucket. Mantenha desativados o URL público do bucket e os domínios personalizados; os cabeçalhos HTTP e as comparações de corpo são a evidência autoritativa do intervalo.

Este exemplo mostra DOCUMENTS conectado ao bucket privado exato. O sufixo do nome gerado será diferente.

A linha do objeto mostra report.txt como text/plain, Standard e 41 B, enquanto Public Access permanece Disabled. Os nomes gerados e as datas são exemplos. O valor agregado Bucket Size pode continuar em 0 B devido ao atraso na atualização; a linha do objeto e a comparação de bytes comprovam que o arquivo existe. Os cabeçalhos HTTP e as comparações do corpo verificam o comportamento condicional e por intervalos.
Remover a aplicação e o bucket remotos
Nesta etapa, você excluirá somente o Worker e os objetos deste laboratório enquanto ainda estiver autorizado. O bucket privado não desaparece quando o Worker é excluído.
npx wrangler delete
Confirme o nome exato do Worker gerado. Exclua explicitamente o único objeto carregado e, depois, exclua o bucket:
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
Somente documents/report.txt foi criado remotamente. Se houver outros objetos, inspecione este bucket exato e confirme a propriedade antes de removê-los.
Atualize as listas de Workers e buckets no Dashboard e execute a verificação de limpeza da plataforma. Falhas de autenticação ou de rede são inconclusivas, não confirmam uma exclusão bem-sucedida.
Revogar as credenciais restantes
Nesta etapa, você revogará o token de gerenciamento deste laboratório na página API Tokens do seu perfil, removerá o segredo local da aplicação e encerrará a autorização da VM. Faça isso somente depois que a verificação de limpeza anterior for aprovada.
rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
Exija loggedIn: false. A revogação do token de gerenciamento é uma verificação manual separada no Dashboard; excluir apenas o arquivo local não o revoga. Mantenha intactos o login normal do Dashboard e os tokens de outros laboratórios.
Resumo
Use metadados do R2 para respostas condicionais, transmita intervalos únicos de bytes, trate solicitações não atendíveis e limpe o serviço privado de downloads.



