通过 Worker 流式传输文档

CloudflareBeginner
立即练习

介绍

支持应用需要接收文档并将其返回,同时不能公开存储桶。你将把私有 R2 存储桶连接到 Worker,实现有大小限制的上传,并向调用方流式传输下载内容。流会在数据块可用时立即传送,而不是先将整个下载内容收集到内存中。

请先完成「整理文档存储桶」以及 Workers 配置和密钥相关课程。本实验使用的全新 VM 已预装 Node.js 22.22.0、Wrangler 4.131.1、合成文档和提供的身份验证模块。该模块使用一次性令牌保护演示端点,避免存储课程暴露不受限制的上传服务。你将在本课程后续学习如何修复应用授权问题。

开始前,你自己的学习账户必须拥有有效的 R2 订阅,并有权限管理新的存储桶和 Worker。请查看 R2 定价;存储和操作费用与 Worker 使用费用分别计量。不需要购买域名。只能使用测试文件,并在实验结束时删除本实验的 Worker、对象和存储桶。每个 VM 都需要单独授权;不会复用之前 VM 中的资源。

连接应用存储桶

在此步骤中,为当前 VM 授权,并为应用创建一个独立的私有存储桶。设备授权用于确认你的学习账户。R2 存储桶管理使用一个仅限该账户的独立 API 令牌。

先启动 Bash,以便使用下面的命令语法,然后进入准备好的项目并检查工具。保持这个终端打开,以便资源名称变量继续有效:

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

在你自己的浏览器中使用显示的设备代码完成授权。授予同意前,确认学习账户以及请求的账户和用户读取权限范围:

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

确认输出中的 loggedIn: true。即使只列出一个账户,也要读取账户名称。将下面的 YOUR_ACCOUNT_ID 替换为该账户实际的 32 位 ID。openssl rand -hex 6 会生成 12 个随机十六进制字符,因此本实验不会与之前的运行发生名称冲突。下面的 here-document 会写入标准配置文件;Shell 会将变量值替换到文件中。

ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r02-$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

要管理存储桶,请打开 Cloudflare 个人资料中的 API Tokens 页面,并创建一个以本实验命名的自定义令牌。授予 Account → Workers R2 Storage → Edit 权限,并将 Account Resources 限制为你保存的 ID 所对应的学习账户。设置较短的过期时间。不要包含其他账户或无关权限。此管理令牌用于管理存储桶,包括创建和删除。在本实验中,Worker 通过 DOCUMENTS 绑定访问 R2 对象。

将令牌复制一次到这个隐藏的 VM 提示符中。umask 077 会将文件权限限制为当前用户;read -s 会隐藏输入内容。该文件使用 Wrangler 的标准令牌变量,并且会被 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

仅在 R2 管理命令中使用 --env-file=.env.management;普通的 whoami 仍会检查 VM 的设备授权。

--env-file 放在每条 Wrangler 命令的末尾,避免它的文件参数列表把命令名称也当作参数。创建每个存储桶后,如果 Wrangler 询问是否向配置添加绑定,请输入 n 并按 Enter。配置中已经包含所需的绑定。

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

列出你的存储桶,并找到刚生成的准确名称。其他存储桶属于其他工作,请不要修改。

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

在 Dashboard 中打开 Storage & databases → R2 → Overview,选择这个准确的存储桶,并检查其空对象列表。在存储桶设置中,保持公共开发 URL 和自定义域名禁用。Dashboard 中显示的存储桶名称可以确认对象身份;后续下载检查会验证存储的字节内容。

Worker 脚本权限用于支持部署。KV 权限用于支持 Wrangler 的删除记录;本实验不会创建 KV 命名空间。R2 管理令牌仍然是一个独立的、限定账户范围的凭据。

实现有大小限制的上传和流式下载

在此步骤中,将 DOCUMENTS 配置绑定转换为对象操作。绑定是 Cloudflare 提供给 Worker 的运行时对象。env.DOCUMENTS 指向按名称配置的私有存储桶;Worker 不需要 S3 密钥即可使用它。

提供的 src/auth.js 会检查一次性 bearer 令牌。当前路由只接受简单的 .txt 文档名称。PUT 会替换所选键中的字节。此示例最多允许 1 MiB(1,048,576 字节),也适用于省略长度标头的客户端。上传数据块只会收集到这个大小限制以内,因此 R2 可以接收一个长度已知的请求体。下载时会将 object.body 直接传给响应,并保持流式传输。

使用下面的 here-document 编写处理程序:

cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
const MAX_BYTES = 1024 * 1024;
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path === "/health" && request.method === "GET") return new Response("ok");
    if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
    if (!/^\/documents\/[a-z0-9-]+\.txt$/.test(path)) return new Response("Not found", { status: 404 });
    const key = path.slice(1);
    if (request.method === "PUT") {
      if (Number(request.headers.get("Content-Length")) > MAX_BYTES)
        return new Response("Too large", { status: 413 });
      // Count actual bytes too: a request may omit Content-Length.
      const reader = request.body?.getReader();
      if (!reader) return new Response("Body required", { status: 400 });
      const chunks = [];
      let total = 0;
      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;
        total += value.byteLength;
        if (total > MAX_BYTES) {
          await reader.cancel();
          return new Response("Too large", { status: 413 });
        }
        chunks.push(value);
      }
      const bytes = new Uint8Array(total);
      let offset = 0;
      for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
      await env.DOCUMENTS.put(key, bytes, { httpMetadata: { contentType: "text/plain" } });
      return new Response("Stored", { status: 201 });
    }
    if (request.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, PUT" } });
    const object = await env.DOCUMENTS.get(key);
    if (object === null) return new Response("Not found", { status: 404 });
    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set("ETag", object.httpEtag);
    headers.set("Cache-Control", "private, no-store");
    return new Response(object.body, { headers });
  }
};
JS

对于不存在的键,get() 会返回 null;读取对象主体前要先处理这种情况。writeHttpMetadata 会恢复保存的内容类型,而 httpEtag 已经使用正确的引号格式。private, no-store 可避免这些受保护的文档进入共享缓存。

.dev.vars 中创建一个随机的应用令牌,Wrangler 会在本地开发时加载它。这是实验专用的合成凭据,与 Cloudflare 账户凭据分开:

umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars

检查 Wrangler 是否可以在不部署的情况下打包代码。平台检查会启动一个独立的临时本地运行时,并使用新的合成数据验证精确字节、两种大小限制路径,以及不会生成超大对象:

npx wrangler deploy --dry-run

测试本地存储边界

在此步骤中,让 Worker 使用本地 R2 存储运行。默认情况下,wrangler dev 使用本地模拟,因此这些请求不会在云端创建对象。在后台运行开发服务器;$! 会记录该后台任务的进程 ID,以便后续清理。

npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!

等待 dev.log 报告服务器已就绪,然后将一次性应用令牌加载到此终端中。不要打印令牌。

cat dev.log
set -a
source .dev.vars
set +a

上传并下载准备好的文件。--data-binary 会保留文件字节;-o 会保存下载内容。

curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt http://127.0.0.1:8787/documents/report.txt
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o local-download.txt
cmp document.txt local-download.txt

上传必须返回 201 Stored,并且静默完成成功的比较。测试不存在的键,以及一个超过大小限制 1 字节的上传。Python 只会创建一个大小受限的测试文件:

curl -i -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/missing.txt
python3 -c "open('oversized.txt','wb').write(b'x' * (1024 * 1024 + 1))"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt http://127.0.0.1:8787/documents/large.txt

必须得到 404 Not found413 Too large。这些 curl 调用会有意省略 --fail,这样预期的 HTTP 错误仍会显示出来。代理返回的错误 HTML 页面不是应用响应。停止本地服务器前,先运行平台检查。

部署并验证私有存储桶集成

在此步骤中,在真实 R2 上重复文档工作流。本地成功不能证明远程绑定或账户归属配置正确。

停止开发服务器并发布 Worker:

kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler deploy

使用标准批量命令上传应用密钥。部署不会自动上传 .dev.vars

npx wrangler secret bulk .dev.vars

从部署输出中复制准确的 HTTPS workers.dev URL,填入 BASE_URL,末尾不要加斜杠。等待 /health 返回 ok;如果部署仍在传播,请在最多 1 分钟内重复读取。

BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"

将报告上传到远程存储桶,下载并进行比较:

curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt "$BASE_URL/documents/report.txt"
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote-download.txt
cmp document.txt remote-download.txt

必须得到 201 Stored,并且字节内容完全一致。针对公共端点重复负面测试:

curl -i "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/missing.txt"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt "$BASE_URL/documents/large.txt"

必须得到 401 Unauthorized404 Not found413 Too large。在 Dashboard 中打开此 Worker 并检查其 R2 绑定;然后打开准确的存储桶,找到 documents/report.txt。其公共开发 URL 和自定义域名仍应保持禁用。Worker 提供访问路径;存储桶保持私有,并不意味着每个 Worker 路由都会自动安全。

Worker 的 DOCUMENTS 绑定连接到私有 R2 存储桶

DOCUMENTS 行将此 Worker 连接到指定存储桶。示例中的自动生成资源名称与你的名称不同。

通过 Worker 上传到私有 Standard 存储桶的报告

对象行显示 report.txt、text/plain 和 41 B,公开访问仍为 Disabled。名称和日期仅为示例。Bucket Size 可能因更新延迟仍显示 0 B;对象行和成功下载才证明报告已存在。

删除远程应用和存储桶

在此步骤中,在授权仍有效时,只删除本实验的 Worker 和对象。删除 Worker 不会自动删除私有存储桶。

npx wrangler delete

确认准确生成的 Worker 名称。先显式删除已上传的对象,再删除存储桶:

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

超大请求不应创建 documents/large.txt。如果存储桶意外不为空,只检查这个存储桶,并在诊断大小限制契约失败后删除准确的合成键。出现这种修复操作,说明前面的功能检查尚未通过。

在 Dashboard 中刷新 Worker 和存储桶列表,并运行平台清理检查。身份验证或网络失败只能说明结果不确定,不能视为删除成功。

撤销剩余凭据

在此步骤中,在个人资料的 API Tokens 页面撤销本实验的管理令牌,删除本地应用密钥,并关闭 VM 授权。只有在前面的清理检查通过后,才能执行这些操作。

rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true

确认输出中的 loggedIn: false。撤销管理令牌是 Dashboard 中单独的手动检查点;仅删除本地文件并不会撤销令牌。保留普通的 Dashboard 登录状态以及其他实验的令牌不变。

总结

将私有 R2 存储绑定到 Worker,接受有大小限制的上传,以流的方式传输精确的文档字节,处理错误,并删除所拥有的云资源。