Skip to main content

R2对象存储大文件上传

R2对象存储大文件上传

R2 是 Cloudflare 推出的对象存储服务,主打零出口费用(也就是免下载流量费)和与 Amazon S3 兼容的 API,适合存储大量数据且需频繁访问的场景。

一、定价详情

官方R2定价细节可以查看:定价 ·Cloudflare R2文档

简化表格

类别说明免费额度超出部分费用
存储存储空间10GB/月每增加 1GB 收费 0.015美元(约0.015 美元(约 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)
})