整理文档存储桶

CloudflareBeginner
立即练习

介绍

你的支持团队需要一个小型文档存储空间。对象是文件的字节和元数据;存储桶用于存放对象;是对象的完整名称。键中的斜杠可以形成有用的前缀,但不会创建普通文件系统目录。你将创建一个私有存储桶,上传两个合成文档,检查它们的元数据,下载完全相同的字节,然后在清理之前只删除选定的文档。

请先完成 将 LabEx 连接到你的 Cloudflare 账户。该实验会介绍 LabEx 终端、设备授权、学习账户确认和账户 ID 配置。本实验从 /home/labex/project/r2-lab 独立开始,其中已准备好 Node.js 22.22.0、Wrangler 4.131.1 和 AWS SDK 3.888.0。在你自己的计算机上,先安装 Node.js,再通过 npm install 将 Wrangler 和 AWS SDK 安装为项目依赖项。

开始前:你的学习账户必须拥有有效的 R2 订阅。Cloudflare 的 R2 设置包含结账流程;如果 R2 尚未启用,请自行查看该流程。Free 账户不会自动启用 R2。请阅读 定价,了解存储和操作费用。本练习使用很小的测试文件,不需要购买域名。你需要存储桶管理权限,以及创建限定到这个新存储桶的用户 R2 令牌的权限。保持公共访问处于禁用状态。不要将凭据粘贴到本课程、聊天或屏幕截图中。

创建私有文档存储桶

在此步骤中,你将授权此 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
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-r01-$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

要管理存储桶,请打开 Cloudflare 个人资料中的 API Tokens 页面,并创建一个以本实验命名的自定义令牌。授予 Account → Workers R2 Storage → Edit 权限,并将 Account Resources 限定为你保存的那个学习账户。设置较短的过期时间。不要包含其他账户或无关权限。此账户级权限可以创建和删除存储桶;下一步使用的仅限对象操作的令牌无法执行这些操作。

将令牌复制一次到此隐藏的 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 中显示存储桶名称可以确认对象身份;稍后的下载检查会验证已存储的字节。

没有对象的私有 Standard 存储桶

示例显示 Standard 存储类型和已关闭的公开访问(Public Access Disabled)。你生成的存储桶名称会不同。

使用元数据上传文档

在此步骤中,你将只向 SDK 授予访问这个存储桶的权限,并存储两个文档。Content type 用于告诉客户端如何解释字节;自定义元数据用于在对象旁边存储你定义的小型标签。这两者都不是访问控制规则。

S3 兼容 API 为标准存储 SDK 提供访问 R2 的能力。它使用单独的访问密钥对,而不是 Wrangler 的设备令牌。在 R2 Overview 中,使用 Account Details → Manage API Tokens,然后创建一个以本实验生成的资源名称命名的 User API token。选择 Object Read & Write,将其限定到这个名称完全匹配的新存储桶;如果表单提供了过期时间,请选择较短的过期时间。不要选择所有存储桶或 Admin 访问权限。在保存这次一次性显示的密钥之前,请保持令牌页面打开。

在 VM 中使用下面的 Bash 提示符。read -s 会隐藏输入;umask 077 会让凭据文件只有当前用户可读。这些名称是 AWS SDK 使用的标准环境变量。分别将 Access Key ID 和 Secret Access Key 粘贴到对应提示符中,然后按 Enter。不要粘贴通用 API 令牌的值。

在令牌表单的 TTL 中选择 24 hours,创建前再次核对存储桶名称和 Object Read & Write 权限。实验结束时撤销令牌;自动到期只是后备措施。

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

编写一个可重复使用的标准 SDK 客户端。SDK 要求提供区域字符串;R2 使用 auto。读取现有配置,可以确保 CLI 和 SDK 操作都指向同一个账户和存储桶。

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

现在创建上传程序。PutObjectCommand 会将字节存储到指定的键中。两个文档都是测试文件;保留的手册将证明,之后选择性删除一个对象不会删除其他无关的键。

cat > upload.mjs <<'JS'
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
await s3.send(new PutObjectCommand({
  Bucket, Key: "documents/report.txt", Body: readFileSync("document.txt"),
  ContentType: "text/plain", Metadata: { team: "blue", revision: "1" }
}));
await s3.send(new PutObjectCommand({
  Bucket, Key: "retained/handbook.txt", Body: readFileSync("retained.txt"),
  ContentType: "text/plain"
}));
console.log("Uploaded two synthetic documents");
JS

--env-file 会加载凭据值,但不会打印这些值:

node --env-file=.env.s3 upload.mjs

只有在两个带 await 的 API 调用都完成后,才会打印成功消息。平台检查会独立读取实际对象及其元数据。

列出元数据并比较下载的字节

在此步骤中,你将检查对象键,而无需下载每个对象,然后获取报告。ListObjectsV2 用于列出键,HeadObject 仅获取元数据。这个小型存储桶只需要一个列表页面;在生产环境中,如果 IsTruncatedtrue,必须使用延续令牌继续获取后续页面。

cat > inspect.mjs <<'JS'
import { ListObjectsV2Command, HeadObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { writeFileSync } from "node:fs";
import { s3, Bucket } from "./storage.mjs";
const page = await s3.send(new ListObjectsV2Command({ Bucket }));
console.log(page.Contents.map(object => object.Key));
const metadata = await s3.send(new HeadObjectCommand({ Bucket, Key: "documents/report.txt" }));
console.log({ contentType: metadata.ContentType, metadata: metadata.Metadata });
const object = await s3.send(new GetObjectCommand({ Bucket, Key: "documents/report.txt" }));
writeFileSync("download.txt", await object.Body.transformToByteArray());
JS
node --env-file=.env.s3 inspect.mjs

列表中应包含 documents/report.txtretained/handbook.txt。报告的内容类型应为 text/plain,并包含 team: bluerevision: 1。元数据输出的顺序可能不同。

cmp 会比较字节;如果文件匹配,则不会打印任何内容。只有比较成功时,才会显示下面的消息:

cmp document.txt download.txt && printf "Downloaded bytes match\n"

刷新同一个存储桶的 Dashboard 对象列表,并打开报告的详细信息。将其中的键和内容类型与 SDK 输出进行比较。如果当前 Dashboard 不显示自定义元数据字段,请使用 CLI 输出作为元数据证据。

报告对象的类型、自定义元数据和预览

示例显示 text/plain、revision 为 1、team 为 blue,以及合成报告的预览。你的存储桶名称和创建日期会不同。

仅删除选定的报告

在此步骤中,你将删除一个完整的对象键,同时保留手册。前缀不是可以递归删除的目录;将报告的完整键准确传递给 API。

cat > remove-report.mjs <<'JS'
import { DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { s3, Bucket } from "./storage.mjs";
await s3.send(new DeleteObjectCommand({ Bucket, Key: "documents/report.txt" }));
const page = await s3.send(new ListObjectsV2Command({ Bucket }));
console.log(page.Contents.map(object => object.Key));
JS
node --env-file=.env.s3 remove-report.mjs

此时只应剩下 retained/handbook.txt。平台检查还会下载手册,以确认其内容保持不变。在继续彻底清理之前,先运行该检查。

清理你创建的存储桶

在此步骤中,你将删除剩余对象,然后删除空存储桶。在确认远程删除完成之前,保持凭据有效。

cat > cleanup.mjs <<'JS'
import { DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { s3, Bucket } from "./storage.mjs";
await s3.send(new DeleteObjectCommand({ Bucket, Key: "retained/handbook.txt" }));
const page = await s3.send(new ListObjectsV2Command({ Bucket }));
console.log("Remaining objects:", page.KeyCount);
JS
node --env-file=.env.s3 cleanup.mjs

确认输出为 Remaining objects: 0。如果你打开了新终端,请从配置中读取生成的存储桶名称;node -p 会打印该字段。

BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management

出现提示时,只确认名称完全匹配的实验存储桶。再次列出存储桶;成功的列表中不应再出现该名称。身份验证错误或网络错误不能证明删除成功。

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

刷新同一个 Dashboard 列表,然后在仍处于登录状态时运行此步骤的平台检查。

撤销实验凭据并退出登录

在此步骤中,你将关闭本练习留下的访问权限。在 R2 API Tokens 页面中,只撤销以本实验命名的对象令牌。在个人资料的 API Tokens 页面中,撤销为本实验创建的另一个 R2 管理令牌。删除存储桶不会撤销令牌,Wrangler 退出登录也不会撤销 S3 凭据。

撤销令牌后,删除本地凭据文件,并退出此 VM 的登录状态:

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

检查结构化身份信息。退出登录后出现非零状态是预期行为:

npx wrangler whoami --json || true

确认 loggedIn: false;保留普通的 Dashboard 登录状态。平台会检查本地凭据是否已删除以及 Wrangler 是否已退出登录。在本实验中,两个令牌的撤销都必须通过 Dashboard 手动完成;不能仅根据文件删除推断令牌已撤销。

总结

你创建了一个私有 R2 存储桶,存储了对象字节和元数据,列出并下载了文档,验证了选择性删除,并清理了存储桶访问权限。