前言

twikoo的评论需要用到的上传图床基本都有各种限制,比如sm.ms只提供5GB的存储,7bu图床需要花钱买,qcloud是腾讯的,不太想用而且要花钱,lsky-pro兰空图床部署需要用到数据库,可是我已经决定用hexo+twikoo搭建一个serverless博客了,还要用到数据库,还需要公网ip服务器,这就本末倒置了。

转念一想既然可以对接兰空图床自定义url,那么我就写一个api兼容兰空图床的api,对接cloudflare r2,这样不就变成免费的10GB额度方案了吗

于是我决定写一个cloudflare worker + cloudflare r2去对接目前lsky-pro对应的端口,然后填上url,返回对应结果

分析接口返回的数据

查看twikoo源码,对应的上传图床部分

  async uploadImageToLskyPro ({ photo, fileName, config, res, imageCdn }) {
    // 自定义兰空图床(v2)URL
    const formData = new FormData()
    formData.append('file', fn.base64UrlToReadStream(photo, fileName))
    if (process.env.TWIKOO_LSKY_STRATEGY_ID) {
      formData.append('strategy_id', parseInt(process.env.TWIKOO_LSKY_STRATEGY_ID))
    }
    const url = `${imageCdn}/api/v1/upload`
    let token = config.IMAGE_CDN_TOKEN
    if (!token.startsWith('Bearer')) {
      token = `Bearer ${token}`
    }
    const uploadResult = await axios.post(url, formData, {
      headers: {
        ...formData.getHeaders(),
        Authorization: token
      }
    })
    if (uploadResult.data.status) {
      res.data = uploadResult.data.data
      res.data.url = res.data.links.url
    } else {
      throw new Error(uploadResult.data.message)
    }
  }

直接定位,接受一个form传参
formData.append('file', fn.base64UrlToReadStream(photo, fileName))
需要的返回值为
uploadResult.data.status
uploadResult.data.data.links.url

返回示例

{
  "status": true,
  "data": {
      "links" : {
        "url": "<r2图床地址>"
      }
  }
}
endpointmethoddata
/api/v1/uploadPOST同上

这里还可以加个auth请求头认证防止盗用

分析完了接下来开始写worker

worker代码

首先创建一个新项目

wrangler init twikoo-worker

修改wrangler.toml的配置,让其绑定r2存储桶及变量

#:schema node_modules/wrangler/config-schema.json
name = "twikoo-worker"
main = "src/index.js"
compatibility_date = "2024-12-05"
compatibility_flags = ["nodejs_compat"]

[observability]
enabled = true

[vars]
BUCKET_URL = "https://bucket.voidval.com"
# 修改你的bucket url,指向你的r2 存储桶

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "bucket1"  # 修改为你图床的bucket名称

根据刚才的接口写worker,修改twikoo-worker/src/index.js

const hasValidHeader = (request, env) => {
    return request.headers.get("Authorization") === "Bearer " + env.AUTH_KEY_SECRET;
  };
  
  
  export default {
    async fetch(request, env, ctx) {
      const url = new URL(request.url);
      if (!hasValidHeader(request, env))
          return new Response("Not permitted.", { status: 401 });
      if (request.method !== "POST") return new Response("Method not permitted.", { status: 401 });
      if (url.pathname !== "/api/v1/upload"){
          return new Response("Path not permitted", { status: 401 });
      }
  
      const formData = await request.formData()
      const photo = await formData.get('file')
      if (!photo) return new Response("Invalid File upload.", { status: 401 });
      await env.MY_BUCKET.put("comment/upload/" + photo.name, photo.stream());
      const responseObject = {
            "status": true,
            "data": {
                "links" : {
                  "url": env.BUCKET_URL + "/comment/upload/" + photo.name
                }
            }
        }
  
      const headers =  {
          'Access-Control-Allow-Origin': '*', // Or your specific origin
          'content-type': 'application/json;charset=UTF-8',
      }
  
      return Response.json(responseObject, {
          headers: headers
      });
  
    },
  };

这里对r2存储桶的操作是根据cloudflare api文档找到的,英语不好很麻烦所以一定要学好英语

现在带着大家分析一下api文档,如果你对此不感兴趣可以跳过

r2 api文档定义

看这个put的部分,很简单,就一句话await env.MY_BUCKET.put(key, request.body);

很明显这里是向r2存储桶添加文件的api,如果你很熟悉cloudflare的话,可以知道env.MY_BUCKET就是下文wrangler.toml绑定的存储桶,MY_BUCKET就是名称
在上面这里有写

这个binding指向的就是在代码里能调用的对象,bucket_name则是存储桶的实际名称

然后看putapi定义
put api定义

可以看到接收keyvalue还有个可选的R2PutOptions参数,并返回Promise R2Object对象(我不是学前端的不知道说的对不对,总之大概是这么个意思)

很容易就推理出key就是存入存储桶的名称,value就是文件二进制数据流,但问题是保存路径该怎么存储呢?比如我想保存到存储桶下的comment/upload文件夹下

我当时的思路是看这个R2PutOptions,但很遗憾没有相关的选项
没有设置路径相关的选项

那该怎么办呢?这里我靠经验把存储桶的key加上了路径await env.MY_BUCKET.put("comment/upload/" + photo.name, photo.stream());

尝试了一下发现还真能改变路径。看来有些时候确实需要一点误打误撞

以上就是worker的逻辑实现

接下来添加secret token

wrangler secret put AUTH_KEY_SECRET

输入你的auth token,记录下这个值,后面twikoo填入配置时要用

wrangler deploy

用postman尝试一下

postman填入token

填入key file

选择form-data,然后选择文件类型filekeyfile(根据前面的formData.append('file', fn.base64UrlToReadStream(photo, fileName)))

查看结果

现在去尝试将url填到twikoo
填入worker地址和token

保存后尝试上传图片

可以看到上传成功了

上传成功

至此你就拥有了一个10GB,且不需要任何公网流出流量费用的,读取写入免费额度达到百万级的twikoo评论图床

虽然只比sm.ms多5G,但也很多了_(:з)∠)_

而且在兼容s3的存储桶中也比较方便管理

理论上也适用各种云函数severless方案,只需要对接存储桶即可

github源码仓库点此跳转