worker 添加一个环境变量 AUTH_KEY_SECRET 用于 CRUD 操作 按需求修改 ALLOW_ORIGINS 用以解决跨源问题 import type { BodyInit, ExportedHandler, Headers, R2Bucket, R2Object, R2Objects, Request, Response } from '@cloudflare/workers-types' interface Env { AUTH_KEY_SECRET: string R2: R2Bucket } /** 统一响应函数 */ class CustomResponse extends Response { constructor(request: Request, body: BodyInit | null = null, init: ResponseInit = {}) { init.headers = Object.assign({}, init.headers, { 'Access-Control-Allow-Headers': 'X-R2-SECRET, Content-Type', 'Access-Control-Allow-Methods': 'GET, PUT, DELETE', Allow: 'GET, PUT, DELETE' }) const ALLOW_ORIGINS = ['https://blog.flysky.xyz', 'http://localhost:3000'] const origin = request.headers.get('Origin') || '' if (ALLOW_ORIGINS.some(it => origin.startsWith(it))) { Reflect.set(init.headers, 'Access-Control-Allow-Origin', origin) } super(body, init) } } export default { async fetch(request, { R2, AUTH_KEY_SECRET }) { try { // 预请求 if (request.method == 'OPTIONS') return new CustomResponse(request) const url = new URL(request.url) const key = decodeURIComponent(url.pathname.slice(1)) // 文件直链 if (request.headers.get('X-R2-SECRET') != AUTH_KEY_SECRET) { const res = await R2.get(key) if (!res) return new CustomResponse(request, `${key} Not Found`, { status: 404 }) if (request.headers.get('If-None-Match') == res.etag) return new CustomResponse(request, null, { status: 304 }) const headers = new Headers() res.writeHttpMetadata(headers) return new CustomResponse(request, res.body, { headers: { ...headers, 'Cache-Control': 'public, max-age=2592000, immutable', // 缓存一个月,过期后再重新验证 ETag: res.etag } }) } // CRUD let res: R2Object | R2Objects | null = null switch (request.method) { case 'GET': res = await R2.list({ delimiter: '/', // @ts-ignore include: ['customMetadata', 'httpMetadata'], prefix: key || undefined }) break case 'PUT': const formData = await request.formData() const { key: _key, blob, metadata, sha1 = '' } = Object.fromEntries(formData) res = await R2.put(_key, blob, { sha1, customMetadata: JSON.parse(metadata) }) break case 'DELETE': await R2.delete(key) break default: return new CustomResponse(request, 'Method Not Allowed', { status: 405 }) } return new CustomResponse(request, JSON.stringify(res || {}), { headers: { 'Content-Type': 'application/json' } }) } catch (error) { return new CustomResponse(request, (error as Error).message, { headers: { 'Content-Type': 'application/json' }, status: 500 }) } } } satisfies ExportedHandler<Env> 使用 注意代码高亮部分,自行修改。 import type { R2Object, R2Objects } from '@cloudflare/workers-types' /** * 计算 hash * @default * algorithm = 'SHA-1' */ const calculateBlobAlgorithm = async (blob: Blob, algorithm: `SHA-${1 | 256 | 384 | 512}` = 'SHA-1') => { const buffer = await blob.arrayBuffer() const data = new Uint8Array(buffer) const md5 = await crypto.subtle.digest(algorithm, data) return Array.from(new Uint8Array(md5), it => it.toString(16).padStart(2, '0')).join('') } export class R2 { static #url = process.env.NEXT_PUBLIC_R2_URL static #headers = { 'X-R2-SECRET': process.env.NEXT_PUBLIC_R2_SECRET } /** 获取直链 */ static get(key: string) { return `${this.#url}/${key}` } /** 获取目录结构 */ static async list(key: string) { const url = new URL(key, this.#url) const res = await fetch(url, { headers: this.#headers, method: 'GET' }) const data: R2Objects = await res.json() return data } /** * 覆盖、新增 * @default * metadata = {} */ static async put({ blob, key, metadata = {} }: { blob: Blob; key: string; metadata?: Record<string, string | number> }) { const formData = new FormData() formData.set('blob', blob) formData.set('key', key) formData.set('metadata', JSON.stringify(metadata)) formData.set('sha1', await calculateBlobAlgorithm(blob)) const res = await fetch(this.#url, { body: formData, headers: this.#headers, method: 'PUT' }) const data: R2Object = await res.json() return data } /** 删除 */ static async delete(key: string) { const url = new URL(key, this.#url) await fetch(url, { headers: this.#headers, method: 'DELETE' }) } } Use R2 from Workers