R2对象存储大文件上传
R2对象存储大文件上传
R2 是 Cloudflare 推出的对象存储服务,主打零出口费用(也就是免下载流量费)和与 Amazon S3 兼容的 API,适合存储大量数据且需频繁访问的场景。
一、定价详情
官方R2定价细节可以查看:定价 ·Cloudflare R2文档
简化表格
| 类别 | 说明 | 免费额度 | 超出部分费用 |
|---|---|---|---|
| 存储 | 存储空间 | 10GB/月 | 每增加 1GB 收费 15/TB) |
| A类操作 | 上传、列出 | 100 万次/月 | 每增加 100 万次 收费 $4.50 美元 |
| B类操作 | 下载、读取 | 1000 万次/月 | 每增加 1000 万次 收费 $0.36 美元 |
| 出口流量 | 访问数据时的流量 | 全免 | 无任何费用 |
二、大文件上传
单个文件 300MB 内可以通过网页上传,大于300MB的需要分片上传。
js 中的上传下载的 token 需要到cloudflare r2 api 中手动申请。
这个脚本上传大文件时,会自动计算文件的 sha256,自动存入自定义元数据中,下载完成后可选是否下载一遍校验文件完整性
/**
* R2 大文件上传 + SHA256 校验(完整示例)
* Node.js >= 18
*/
import { S3Client, GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"
import { Upload } from "@aws-sdk/lib-storage"
import fs from "fs"
import crypto from "crypto"
import readline from "readline"
// ================== 配置区 ==================
const CONFIG = {
ACCOUNT_ID: "***************************",
ACCESS_KEY: "******************************",
SECRET_KEY: "*****************************************",
BUCKET: "r2-opendesk",
FILE_PATH: "./virtio-win-0.1.271.iso",
KEY: "virtio-win-0.1.271.iso",
// 上传性能参数
PART_SIZE: 10 * 1024 * 1024, // 10MB
QUEUE_SIZE: 5
}
// ================== 初始化客户端 ==================
const client = new S3Client({
region: "auto",
endpoint: `https://${CONFIG.ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CONFIG.ACCESS_KEY,
secretAccessKey: CONFIG.SECRET_KEY
}
})
// ================== 工具函数 ==================
// 计算本地文件 SHA256
async function calculateSHA256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash("sha256")
const stream = fs.createReadStream(filePath)
stream.on("data", (chunk) => hash.update(chunk))
stream.on("end", () => resolve(hash.digest("hex")))
stream.on("error", reject)
})
}
// CLI 询问
function ask(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close()
resolve(answer.trim().toLowerCase())
})
})
}
// 下载并计算远程 SHA256
async function verifyRemoteFile(bucket, key, localHash) {
console.log("\n开始下载远程文件进行 SHA256 校验...")
const res = await client.send(new GetObjectCommand({
Bucket: bucket,
Key: key
}))
const hash = crypto.createHash("sha256")
let loaded = 0
const start = Date.now()
for await (const chunk of res.Body) {
hash.update(chunk)
loaded += chunk.length
// 每 100MB 打印一次
if (loaded % (100 * 1024 * 1024) < chunk.length) {
console.log(`已下载 ${(loaded / 1024 / 1024).toFixed(2)} MB`)
}
}
const remoteHash = hash.digest("hex")
const cost = ((Date.now() - start) / 1000).toFixed(2)
console.log("\n========== 校验结果 ==========")
console.log("本地 SHA256 :", localHash)
console.log("远程 SHA256 :", remoteHash)
console.log("耗时:", cost, "秒")
if (remoteHash === localHash) {
console.log("✅ 文件完全一致")
} else {
console.log("❌ 文件不一致(可能损坏)")
}
}
// 查看元数据(轻量验证)
async function checkMetadata(bucket, key) {
const res = await client.send(new HeadObjectCommand({
Bucket: bucket,
Key: key
}))
console.log("\nR2 元数据:")
console.log(res.Metadata)
}
// ================== 上传主流程 ==================
async function main() {
console.log("开始计算本地文件 SHA256...")
const sha256 = await calculateSHA256(CONFIG.FILE_PATH)
console.log("本地 SHA256:", sha256)
console.log("\n开始上传到 R2...")
const upload = new Upload({
client,
params: {
Bucket: CONFIG.BUCKET,
Key: CONFIG.KEY,
Body: fs.createReadStream(CONFIG.FILE_PATH),
Metadata: {
sha256
}
},
queueSize: CONFIG.QUEUE_SIZE,
partSize: CONFIG.PART_SIZE
})
// 上传进度
upload.on("httpUploadProgress", (p) => {
if (p.total) {
const percent = ((p.loaded / p.total) * 100).toFixed(2)
process.stdout.write(`\r上传进度: ${percent}%`)
}
})
await upload.done()
console.log("\n\n上传完成!")
// 查看 metadata
await checkMetadata(CONFIG.BUCKET, CONFIG.KEY)
// 询问是否校验
const answer = await ask("\n是否下载远程文件验证 SHA256?(y/N): ")
if (answer === "y" || answer === "yes") {
await verifyRemoteFile(CONFIG.BUCKET, CONFIG.KEY, sha256)
} else {
console.log("已跳过校验")
}
}
// 执行
main().catch(err => {
console.error("发生错误:", err)
})
三、删除文件
/**
* R2 文件列表 + 选择删除,需要在web页面增加api令牌的获取和删除权限
* 这个脚本是删除文件上传成功的操作
*/
import {
S3Client,
ListObjectsV2Command,
DeleteObjectCommand
} from "@aws-sdk/client-s3"
import readline from "readline"
// ================== 配置 ==================
const CONFIG = {
ACCOUNT_ID: "***************************",
ACCESS_KEY: "******************************",
SECRET_KEY: "*****************************************",
BUCKET: "r2-opendesk"
}
// ================== 客户端 ==================
const client = new S3Client({
region: "auto",
endpoint: `https://${CONFIG.ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CONFIG.ACCESS_KEY,
secretAccessKey: CONFIG.SECRET_KEY
}
})
// ================== CLI 输入 ==================
function ask(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close()
resolve(answer.trim())
})
})
}
// ================== 获取所有文件 ==================
async function listAllObjects(bucket) {
let continuationToken = undefined
let allObjects = []
do {
const res = await client.send(new ListObjectsV2Command({
Bucket: bucket,
ContinuationToken: continuationToken
}))
if (res.Contents) {
allObjects.push(...res.Contents)
}
continuationToken = res.IsTruncated
? res.NextContinuationToken
: undefined
} while (continuationToken)
return allObjects
}
// ================== 主逻辑 ==================
async function main() {
console.log("正在获取文件列表...")
const objects = await listAllObjects(CONFIG.BUCKET)
if (!objects.length) {
console.log("Bucket 是空的")
return
}
// 按时间排序(最新在前)
objects.sort((a, b) => new Date(b.LastModified) - new Date(a.LastModified))
console.log("\n文件列表:\n")
objects.forEach((obj, index) => {
console.log(
`${index + 1}. ${obj.Key} | ${(obj.Size / 1024 / 1024).toFixed(2)} MB | ${obj.LastModified}`
)
})
// 用户选择
const input = await ask("\n请输入要删除的文件编号(直接回车退出):")
if (!input) {
console.log("已取消")
return
}
const index = parseInt(input) - 1
if (isNaN(index) || index < 0 || index >= objects.length) {
console.log("输入无效")
return
}
const target = objects[index]
console.log(`\n你选择删除: ${target.Key}`)
// 二次确认(防手滑)
const confirm = await ask("确认删除?(y/N): ")
if (confirm.toLowerCase() !== "y") {
console.log("已取消删除")
return
}
// 删除
await client.send(new DeleteObjectCommand({
Bucket: CONFIG.BUCKET,
Key: target.Key
}))
console.log("✅ 删除成功")
}
// 执行
main().catch(err => {
console.error("错误:", err)
})
四、删除上传失败的文件
/**
* R2 文件列表 + 选择删除,需要在web页面增加api令牌的获取和删除权限
* * 这个脚本是删除文件分片上传失败的操作
*/
import {
S3Client,
ListMultipartUploadsCommand,
AbortMultipartUploadCommand
} from "@aws-sdk/client-s3"
import readline from "readline"
// ================== 配置 ==================
const CONFIG = {
ACCOUNT_ID: "***************************",
ACCESS_KEY: "******************************",
SECRET_KEY: "*****************************************",
BUCKET: "r2-opendesk"
}
const client = new S3Client({
region: "auto",
endpoint: `https://${CONFIG.ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CONFIG.ACCESS_KEY,
secretAccessKey: CONFIG.SECRET_KEY
}
})
// CLI
function ask(q) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
return new Promise(r => rl.question(q, a => {
rl.close()
r(a.trim())
}))
}
// ================== 获取未完成上传 ==================
async function listMultipartUploads(bucket) {
const res = await client.send(new ListMultipartUploadsCommand({
Bucket: bucket
}))
return res.Uploads || []
}
// ================== 主逻辑 ==================
async function main() {
console.log("获取未完成的 multipart 上传...")
const uploads = await listMultipartUploads(CONFIG.BUCKET)
if (!uploads.length) {
console.log("没有未完成上传")
return
}
console.log("\n未完成上传列表:\n")
uploads.forEach((u, i) => {
console.log(
`${i + 1}. ${u.Key} | UploadId=${u.UploadId} | ${u.Initiated}`
)
})
const input = await ask("\n输入要清理的编号(回车退出):")
if (!input) return
const index = parseInt(input) - 1
if (isNaN(index) || index < 0 || index >= uploads.length) {
console.log("输入无效")
return
}
const target = uploads[index]
console.log(`\n准备清理: ${target.Key}`)
const confirm = await ask("确认中止上传?(y/N): ")
if (confirm.toLowerCase() !== "y") {
console.log("已取消")
return
}
await client.send(new AbortMultipartUploadCommand({
Bucket: CONFIG.BUCKET,
Key: target.Key,
UploadId: target.UploadId
}))
console.log("✅ 已清理未完成上传")
}
main().catch(console.error)