标签: s3

  • 上传文件到 CloudFlare R2

    上传文件到 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 存储的申请也通过了。必须得说,这些互联网基础设施对我们全栈开发者来说非常重要,而无障碍的域名对目前在国内还是一种奢望,所以用国外服务就成为一种必然。希望未来会更好吧。