上传文件到 CloudFlare R2

上周日突然感染流感,发烧一天多,头昏脑涨两三天,只好请假休养。不知道是不是二阳,懒得测。工作进展不多,周末适当加加班,博客适当划划水,这周就记录一下 CloudFlare R2 上传文件吧,实测效果很好,简单省事好用,推荐大家使用。

CloudFlare R2 兼容 AWS S3 API,但是不需要那么复杂的 IAM 体系,而且直接对接 CloudFlare CDN,我觉得更好用。跟国内的众多云存储比起来,它不需要备案,还提供域名和证书,用起来更简单。所以,如果你的网站或者服务需要一个云存储,但是暂时不想负担太高的成本,我建议先试试 CF R2。

我假设读者已经拥有 CF 账号,在 R2 里创建了 bucket,并且完成了需要的配置(好像只需要配置一个域名)。接下来,我们需要在自己的业务代码里实现上传。

首先,我们要安装 AWS SDK。

pnpm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

接下来,我们需要在项目中建立 S3 配置,其中关键信息都写在 .env 里。

import { S3Client } from '@aws-sdk/client-s3';

const S3 = new S3Client({
  region: 'auto',
  endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY,
    secretAccessKey: process.env.R2_SECRET_KEY,
  },
});

export { S3 };

然后就可以生成预签字的上传路径,相当于提前校验用户身份,这一步需要放在服务器端进行,避免泄漏关键信息。我这里因为业务需求比较简单,没有做复杂的鉴权和检查,如果有需要,就多验证几步。

import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import slugify from 'slugify';
import { H3Event } from 'h3';
import { S3 } from '~/lib/s3';
import { ApiResponse, PreSignedUrl } from '~/types';

export default defineEventHandler(async function (event: H3Event): Promise<ApiResponse<PreSignedUrl>> {
  // 这一步提交的是预处理信息,没有文件本身,所以还是 json
  const json = await readBody(event) as { fileName: string | undefined, fileType: string | undefined };
  const { fileName, fileType } = json;

  // 生成存储对象 key,其实就是文件名
  const objectKey = `${slugify(Date.now().toString())}-${slugify(fileName)}`;

  // 生成预签名 url
  const preSignedUrl = await getSignedUrl(S3, new PutObjectCommand({
    Bucket: process.env.PUBLIC_S3_BUCKET_NAME,
    Key: objectKey,
    ContentType: fileType,
    ACL: 'public-read',
  }), {
    // 此 URL 5分钟内有效
    expiresIn: 60 * 5, // 5 minutes
  });

  return {
    code: 0,
    data: {
      preSignedUrl,
      objectKey,
    },
  };
});

最后,我们就可以上传文件了。

  const response = await $fetch<ApiResponse<PreSignedUrl>>('/api/get-upload-url', {
    method: 'POST',
    body: {
      fileName: file.name,
      fileType: file.type,
    },
  });  
  const { preSignedUrl, objectKey } = response.data as PreSignedUrl;
  const uploadToR2Response = await fetch(preSignedUrl, {
    method: 'PUT',
    headers: {
      'Content-Type': file.type,
    },
    body: file,
  });
  if (!uploadToR2Response.ok) {
    console.error('Failed to upload file to R2');
  }

整个上传过程比较简单,服务器只需要做一些权限校验,剩下来的过程都可以交给 R2。关键是,这样一来,因为不涉及文件操作,就可以放心交给 Serverless 来做,Vercel 友好。上传后的文件自动进入 CF CDN,也很容易使用。

学会使用 R2 之后没多久,我的 Vercel Blob 存储的申请也通过了。必须得说,这些互联网基础设施对我们全栈开发者来说非常重要,而无障碍的域名对目前在国内还是一种奢望,所以用国外服务就成为一种必然。希望未来会更好吧。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

标签:

评论

欢迎吐槽,共同进步

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据