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'
})
}
}