目前支持所有 S3 兼容存储提供商,包括 AWS S3DigitalOcean SpacesCloudflare R2Supabase Storage 等。以下是如何将默认存储提供商切换为 AWS S3 兼容存储的指南。

您可以在官方 AWS 文档或您特定存储提供商的文档中了解更多关于 S3 服务配置的信息。

权限设置

在开始管理文件之前,请确保您已配置好存储。

大多数 S3 兼容存储提供商允许您配置存储桶权限和访问策略。正确设置这些对于保护您的文件和控制访问权限至关重要。

以下是一些关键的安全建议:

  • 默认情况下保持存储桶私有
  • 使用 IAM 角色和策略管理访问
  • 对敏感数据启用服务器端加密
  • 为客户端上传适当配置 CORS 设置
  • 定期审核存储桶权限和访问日志
  • 强烈不建议将存储桶设为公开,因为这可能会暴露敏感数据,导致未经授权的访问和带宽使用产生的意外费用。

有关配置存储桶策略和权限的详细指导,请参阅存储提供商的文档:

1. 更新环境变量

接下来,将这些环境变量添加到.env.local文件中:

apps/app/.env

// Add this:
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_BUCKET_NAME=""
AWS_REGION=""
AWS_ENDPOINT=""

2. 更新现有存储文件

更新 index.tsclient.ts 以使用新的 uploadthing 软件包:

packages/storage/index.ts
export * from './providers/s3';

3. 准备上传端点

概述页面所述,SuperStarter 使用预签名 URL 将文件上传到您的存储提供商。因此,我们首先需要实现 api/signed-upload-url API 路由,以便能够将文件上传到 documents 存储桶。

apps/api/server/api/routes/upload/index.ts
export const uploadsRouter = new Hono().basePath("/uploads").post(
	"/signed-upload-url",
    // using the authMiddleware will make sure the user is authenticated
	authMiddlewareWrap,
	validator(
		"query",
		z.object({
			bucket: z.string().min(1),
			path: z.string().min(1),
		}),
	),
 // ...
	async (c) => {
		const { bucket, path } = c.req.valid("query");
 
  // ...
 
	  const signedUrl = await getSignedUploadUrl(path, { bucket });
		return c.json({ signedUrl });
 
		throw new HTTPException(403);
	},
);

4. 从用户界面上传文件

然后,您就可以用它从前端代码中将文件上传到生成的预指定 URL:

upload.tsx
const upload = useMutation({
    mutationFn: async (data: { file?: File }) => {
      const extension = data.file?.type.split("/").pop();
      const path = `files/${crypto.randomUUID()}.${extension}`;
 
      const { url: uploadUrl } = await handle(api.upload.signedUploadUrl.$get)({
        query: { path },
      });
 
      const response = await fetch(uploadUrl, {
        method: "PUT",
        body: data.file,
        headers: {
          "Content-Type": data.file?.type ?? "",
        },
      });
 
      if (!response.ok) {
        throw new Error("Failed to upload file!");
      }
    },
    onError: (error) => {
      toast.error(error.message});
    },
    onSuccess: async ({ publicUrl, oldImage }, _b, context) => {
      toast.success("File uploaded!");
    },
  });