Convex R2 组件
用 Cloudflare R2 存储并服务文件.
// or @convex-dev/r2/svelte for Svelte!
import { useUploadFile } from "@convex-dev/r2/react";
// Upload files from React
const uploadFile = useUploadFile(api.example);
// ...in a callback
const key = await uploadFile(file);
// Access files on the server
const url = await r2.getUrl(key);
const response = await fetch(url);
看看这个 示例应用程序 一个完整的例子.
先决条件
云浮账户
- 创建 Cloudflare 账户
- 创建 R2 桶
- 将桶名设定为环境变量
R2_BUCKET在您的凸起处 通过部署npx convex env set R2_BUCKET <bucket-name>。 。 。 . - 增加中央资源评估政策
允许GET和PUT从您的 Convex 应用程序请求。 你也可以这样
使用 '*' 来允许所有来源(谨慎使用).
[ { "AllowedOrigins": ["http://localhost:5173"], "AllowedMethods": ["GET", "PUT"], "AllowedHeaders": ["Content-Type"] } ] - 创建 API 令牌
- 在您 Cloudflare 仪表板中的主 R2 页面上,单击 管理 R2 API 键
- 点击 创建 API 脚本
- 编辑符号名称
- 设置权限到 对象读取并写入
- 下 指定桶,选择您在上面创建的桶
- 可选更改 TTL
- 点击 创建 API 脚本
- 在下一个屏幕上,你会得到你以后需要的四个值:
- 调值数字 :
R2_TOKEN - 访问密钥 ID数字 :
R2_ACCESS_KEY_ID - 秘密访问密钥数字 :
R2_SECRET_ACCESS_KEY - 终点数字 :
R2_ENDPOINT
- 调值数字 :
Convex 应用程序
你需要一个Convex App来使用组件. 跟着任何 Convex 快速启动 来设置一个.
安装
安装组件包 :
npm install @convex-dev/r2
创建一个 convex.config.ts 应用程序中的文件 convex/ 文件夹并安装
通过调用 use数字 :
// convex/convex.config.ts
import { defineApp } from "convex/server";
import r2 from "@convex-dev/r2/convex.config.js";
const app = defineApp();
app.use(r2);
export default app;
使用您先前记录的值设置您的 API 证书 :
npx convex env set R2_TOKEN xxxxx
npx convex env set R2_ACCESS_KEY_ID xxxxx
npx convex env set R2_SECRET_ACCESS_KEY xxxxx
npx convex env set R2_ENDPOINT xxxxx
npx convex env set R2_BUCKET xxxxx
注:环境变量只有在不直接通过配置时才需要
页:1 R2 建筑师。 您也可以通过单个值来覆盖
具体的环境变量.
使用多桶
要使用多个 R2 桶, 请另建 R2 不同实例
配置 :
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";
// Primary bucket — reads from R2_* environment variables
const primary = new R2(components.r2);
// Archive bucket — configured explicitly
const archive = new R2(components.r2, {
bucket: process.env.ARCHIVE_BUCKET!,
endpoint: process.env.ARCHIVE_ENDPOINT!,
accessKeyId: process.env.ARCHIVE_ACCESS_KEY_ID!,
secretAccessKey: process.env.ARCHIVE_SECRET_ACCESS_KEY!,
});
每个实例都维持自己的配置和S3客户端. 全部元数据 桶并存于同名Convex表,由桶名索引.
上传文件
文件上传到R2通常使用签名的urls. R2组件提供钩子 对处理整个上传进程的React和Svelte:
- 生成已签名的 url
- 上传文件到 R2
- 在您的 Convex 数据库中存储文件的元数据
重要: 对于在召回中打入的正确类型脚本,请通过 DataModel 键入为通用参数 r2.clientApi<DataModel>()。这将确保 ctx 参数与您的数据库计划一起输入并返回查询 Doc<"tableName"> 改为 GenericDocument。 。 。 .
-
在您的应用程序中的文件里提供 R2 组件客户端的证明
convex/文件夹 :// convex/example.ts import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; import type { DataModel } from "./_generated/dataModel"; export const r2 = new R2(components.r2); // Pass DataModel as a generic type parameter to get proper TypeScript typing // for all callback contexts. Without this, ctx will be typed as GenericDocument // instead of your specific table types. export const { generateUploadUrl, syncMetadata } = r2.clientApi<DataModel>({ checkUpload: async (ctx, bucket) => { // const user = await userFromAuth(ctx); // ...validate that the user can upload to this bucket }, onUpload: async (ctx, bucket, key) => { // ...do something with the key // This technically runs in the `syncMetadata` mutation, as the upload // is performed from the client side. Will run if using the `useUploadFile` // hook, or if `syncMetadata` function is called directly. Runs after the // `checkUpload` callback. }, }); -
使用
useUploadFile在组件中钩入上传文件 :反应 :
// src/App.tsx import { FormEvent, useRef, useState } from "react"; import { useAction } from "convex/react"; import { api } from "../convex/_generated/api"; import { useUploadFile } from "@convex-dev/r2/react"; export default function App() { // Passing the entire api exported from `convex/example.ts` to the hook. // This must include `generateUploadUrl` and `syncMetadata` from the r2 client api. const uploadFile = useUploadFile(api.example); const imageInput = useRef<HTMLInputElement>(null); const [selectedImage, setSelectedImage] = useState<File | null>(null); async function handleUpload(event: FormEvent) { event.preventDefault(); // The file is uploaded to R2, metadata is synced to the database, and the // key of the newly created object is returned. await uploadFile(selectedImage!); setSelectedImage(null); imageInput.current!.value = ""; } return ( <form onSubmit={handleUpload}> <input type="file" accept="image/*" ref={imageInput} onChange={(event) => setSelectedImage(event.target.files![0])} disabled={selectedImage !== null} /> <input type="submit" value="Upload" disabled={selectedImage === null} /> </form> ); }斯维尔特:
<script lang="ts"> import { useUploadFile } from "@convex-dev/r2/svelte"; import { api } from "../convex/_generated/api"; const uploadFile = useUploadFile(api.example); let selectedImage = $state<File | null>(null); async function handleUpload(file: File) { await uploadFile(file); selectedImage = null; } </script> <form onsubmit={() => { if (selectedImage) handleUpload(selectedImage); }} > <input type="file" accept="image/*" onchange={(e) => { selectedImage = e.currentTarget.files?.[0] ?? null; }} disabled={selectedImage !== null} /> <button type="submit" disabled={selectedImage === null}> Upload </button> </form>
使用自定义对象密钥
这个 r2.generateUploadUrl 函数生成一个 uuid 以用作对象键
默认,但如果需要,可以提供自定义密钥。 说明:
generateUploadUrl 函数返回 r2.clientApi 不接受习惯
键,因为此函数是一个从客户端调用到您的突变
不希望您的客户定义您的对象密钥 。 提供自定义密钥
需要自己做个变异 叫做 generateUploadUrl 方法
页:1 r2 举个例子.
// convex/example.ts
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";
export const r2 = new R2(components.r2);
// A custom mutation that creates a key from the user id and a uuid. If the key
// already exists, the mutation will fail.
export const generateUploadUrlWithCustomKey = mutation({
args: {},
handler: async (ctx) => {
// Replace this with whatever function you use to get the current user
const currentUser = await getUser(ctx);
if (!currentUser) {
throw new Error("User not found");
}
const key = `${currentUser.id}.${crypto.randomUUID()}`;
return r2.generateUploadUrl(key);
},
});
从动作存储文件
文件可以直接从使用此选项的动作中存储在 R2 中 r2.store 方法.
当您需要存储生成或下载到的文件时,此选项会有所帮助
服务器侧.
// convex/example.ts
import { internalAction } from "./_generated/server";
import { R2 } from "@convex-dev/r2";
const r2 = new R2(components.r2);
export const store = internalAction({
handler: async (ctx) => {
// Download a random image from picsum.photos
const url = "https://picsum.photos/200/300";
const response = await fetch(url);
const blob = await response.blob();
// This function call is the only required part, it uploads the blob to R2,
// syncs the metadata, and returns the key. The key is a uuid by default, but
// an optional custom key can be provided in the options object. A MIME type
// can also be provided, which will override the type inferred for blobs. A
// Content-Disposition and Cache-Control header can also be provided if
// necessary.
const key = await r2.store(ctx, blob, {
key: "my-custom-key",
type: "image/jpeg",
disposition: "attachment; filename=example.jpg",
cacheControl: "max-age=3600",
});
// Example use case, associate the key with a record in your database
await ctx.runMutation(internal.example.insertImage, { key });
},
});
这个 store 方法 :
- 需要一个
Blob, (中文(简体) ),Buffer,或Uint8Array并存储在R2中 - 将元数据同步到您的 Convex 数据库
- 返回可用于稍后访问文件的密钥
服务文件
存储在 R2 中的文件可以通过生成一个 URL 指向一个 给定文件.
在查询中生成文件 URL
服务文件的最简单方式是返回 URL 和其他需要的数据 根据你的应用程序 从询问和突变.
文件 URL 可以由对象键生成 r2.getUrl 函数
R2组件客户端.
// convex/listMessages.ts
import { components } from "./_generated/api";
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";
const r2 = new R2(components.r2);
export const list = query({
args: {},
handler: async (ctx) => {
// In this example, messages have an imageKey field with the object key
const messages = await ctx.db.query("messages").collect();
return Promise.all(
messages.map(async (message) => ({
...message,
imageUrl: await r2.getUrl(
message.imageKey,
// Options object is optional, can be omitted
{
// Custom expiration time in seconds, default is 900 (15 minutes)
expiresIn: 60 * 60 * 24, // 1 day
},
),
})),
);
},
});
文件 URL 可以用于 img 元素来渲染图像 :
// src/App.tsx
function Image({ message }: { message: { url: string } }) {
return <img src={message.url} height="300px" width="auto" />;
}
从 Cloudflare 的 CDN 通过自定义域服务文件
r2.getUrl() 生成已签名的过期的 S3 URL( 默认为 15分钟) 和
路由直接通向R2的S3 API端点,绕过Cloudflare的边缘缓存. 对于
使用您需要永久的 CDN 标记的 URL( 图片、 相机、 相机、,
您可以将自定义域名连接到您的 R2 桶并构建
从对象键直接取出URL(如cdn.example.com,其中example.com是您拥有的一个域).
设置 R2 自定义域
您首先需要将自定义域与您的 R2 桶连接 - 跟着他们的文件 要开始.
Cloudflare 自动缓存最常见的文件类型 。 它们提供了完整的清单,以及关于如何为不太常见的类型进行缓存的信息 在其文档中。 。 。 .
从对象键构建 CDN URL
// convex/files.ts
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";
import { query } from "./_generated/server";
const r2 = new R2(components.r2);
// Build CDN URL from a stored object key.
function getCdnUrl(key: string): string {
// Replace with your domain
const base = "cdn.example.com"
return `${base}/${key.split('/').map(encodeURIComponent).join('/')`
}
export const listMessages = query({
args: {},
handler: async (ctx) => {
const messages = await ctx.db.query("messages").collect();
return messages.map((message) => ({
...message,
// Return cached, CDN served image
imageUrl: getCdnUrl(message.imageKey),
}));
},
});
说明: 别用那个
r2.dev生产中的公共开发 URL - 它的速率有限且没有被标记. 使用自定义域代替 .
删除文件
存储在 R2 中的文件可以通过
r2.deleteObject 函数,它接受对象键.
// convex/images.ts
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { R2 } from "@convex-dev/r2";
const r2 = new R2(components.r2);
export const deleteObject = mutation({
args: {
key: v.string(),
},
handler: async (ctx, args) => {
return await r2.deleteObject(ctx, args.key);
},
});
访问文件元数据
R2 文件的文件元数据可以从动作中访问 r2.getMetadata数字 :
// convex/images.ts
import { v } from "convex/values";
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";
const r2 = new R2(components.r2);
export const getMetadata = query({
args: {
key: v.string(),
},
handler: async (ctx, args) => {
return await r2.getMetadata(args.key);
},
});
这是返回的文档的一个例子:
{
"ContentType": "image/jpeg",
"ContentLength": 125338,
"LastModified": "2024-03-20T12:34:56Z"
}
返回的文档有以下字段:
ContentType: 如果上传时提供了文件的内容类型ContentLength: 以字节表示的文件大小LastModified: 文件的最后修改日期
列出和介绍元数据
元数据可通过下列方式从动作中列出或标出: r2.listMetadata 财务报告和审定财务报表
r2.pageMetadata。 。 。 .
// convex/example.ts
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";
const r2 = new R2(components.r2);
export const list = query({
args: {
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
return r2.listMetadata(ctx, args.limit);
},
});
export const page = query({
args: {
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return r2.pageMetadata(ctx, args.paginationOpts);
},
});
上传后访问元数据
这个 onSyncMetadata 可使用回调来运行每个元数据后的突变
同步 。 这个 useUploadFile 钩钩每次上传后都会同步元数据, 所以此
函数也会每次运行.
因为这是在元数据同步后运行的 r2.getMetadata 可用于
访问新上传文件的元数据.
// convex/example.ts
import { R2, type R2Callbacks } from "@convex-dev/r2";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
export const r2 = new R2(components.r2);
const callbacks: R2Callbacks = internal.example;
export const { generateUploadUrl, syncMetadata, onSyncMetadata } = r2.clientApi<DataModel>(
{
// Pass the functions from this file back into the component.
// Technically only an object with `onSyncMetadata` is required, the recommended
// pattern is just for convenience.
callbacks,
onSyncMetadata: async (ctx, args) => {
// args: { bucket: string; key: string; isNew: boolean }
// args.isNew is true if the key did not previously exist in your Convex R2
// metadata table
const metadata = await r2.getMetadata(ctx, args.key);
// log metadata of synced object
console.log("metadata", metadata);
},
},
);