SkyBlog

使用 worker 管理 cloudflare r2

目前该博客使用的方法,仅作为记录

这篇文章发布于 2024年11月04日,星期一,05:54阅读 ? 次,0 条评论

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