- 后端
- 数据库
【免费下载链接】instant
Instant is the best backend for AI-coded apps. You get auth, permissions, storage, presence, and streams — everything you need to ship apps your users will love.
Instant Storage 是 Instant 提供的开箱即用文件存储能力,让应用可以轻松上传并托管图片、视频、文档等任意类型文件。本文基于仓库中的官方文档(client/www/app/docs/storage/page.md)整理成文,并结合 client/packages/core/src/StorageAPI.ts、client/packages/core/src/index.ts 与 client/packages/admin/src/index.ts 中的真实实现,深入讲解上传、覆盖、查询、删除、更新、链接、权限控制以及 React Native / Admin SDK 的完整用法。读完本文,你将能在自己的 Instant 应用中实现一个可实时同步、带权限校验的文件上传与展示功能。
Storage 快速上手:构建一个实时图片墙
Instant Storage 与数据库深度集成:文件上传后会自动写入$files命名空间,查询、排序、关联与权限规则都可以像操作普通实体一样使用。下面从零开始构建一个"上传并展示图片网格"的完整示例。
1. 创建 Next.js 项目并安装依赖
npx create-next-app instant-storage --tailwind --yes cd instant-storage npm i @instantdb/react2. 初始化 schema 与权限
通过 CLI 初始化 Instant 配置(CLI 的完整用法见 client/www/app/docs/cli/page.md):
npx instant-cli@latest init打开生成的instant.schema.ts,替换为以下内容。核心是声明了一个$files实体——这是 Instant Storage 内置的特殊命名空间,用于描述文件元数据:
import { i } from "@instantdb/react"; const _schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.string(), }), $users: i.entity({ email: i.string().unique().indexed(), }), }, links: {}, rooms: {}, }); // This helps TypeScript display nicer IntelliSense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema;其中path是文件在存储目录中的路径,unique().indexed()保证路径唯一并支持按路径查询;url是服务端生成的可直接用于展示文件内容的下载地址。$users是 Instant 内置的用户实体,在后续权限示例中会用到。
同样打开instant.perms.ts,替换为以下权限规则:
import type { InstantRules } from "@instantdb/react"; // Not recommended for production since this allows anyone to // upload/delete, but good for getting started const rules = { "$files": { "allow": { "view": "true", "create": "true", "delete": "true" } } } satisfies InstantRules; export default rules;该规则允许任何人上传与删除文件,仅适合快速体验,生产环境请参考文末"Storage 权限控制"一节收紧。
把 schema 与权限推送到你的 Instant 应用:
npx instant-cli@latest push3. 实现上传与图片网格
将app/page.tsx替换为以下代码。它演示了 Instant Storage 的三个核心 API:
db.storage.uploadFile(file.name, file, opts)执行实际上传;db.useQuery({ $files: {...} })查询文件列表,上传完成后查询结果会自动更新(实时同步);db.transact(db.tx.$files[image.id].delete())删除文件。
'use client'; import { init, InstaQLEntity } from '@instantdb/react'; import schema, { AppSchema } from '../instant.schema'; import React from 'react'; type InstantFile = InstaQLEntity<AppSchema, '$files'> const APP_ID = process.env.NEXT_PUBLIC_INSTANT_APP_ID; const db = init({ appId: APP_ID, schema }); // `uploadFile` is what we use to do the actual upload! // The `$files` query will automatically update once the upload is complete async function uploadImage(file: File) { try { // Optional metadata you can set for uploads const opts = { // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type // Default: 'application/octet-stream' contentType: file.type, // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition // Default: 'inline' contentDisposition: 'attachment', }; await db.storage.uploadFile(file.name, file, opts); } catch (error) { console.error('Error uploading image:', error); } } function App() { // $files is the special namespace for querying storage data const { isLoading, error, data } = db.useQuery({ $files: { $: { order: { serverCreatedAt: 'asc' }, }, }, }); if (isLoading) { return null; } if (error) { return <div>Error fetching data: {error.message}</div>; } // The result of a $files query will contain objects with // metadata and a download URL you can use for serving files! const { $files: images } = data return ( <div className="box-border bg-gray-50 font-mono min-h-screen p-5 flex items-center flex-col"> <div className="tracking-wider text-5xl text-gray-300 mb-8"> Image Feed </div> <ImageUpload /> <div className="text-xs text-center py-4"> Upload some images and they will appear below! Open another tab and see the changes in real-time! </div> <ImageGrid images={images} /> </div> ); } interface SelectedFile { file: File; previewURL: string; } function ImageUpload() { const [selectedFile, setSelectedFile] = React.useState<SelectedFile | null>(null); const [isUploading, setIsUploading] = React.useState(false); const fileInputRef = React.useRef<HTMLInputElement>(null); const { previewURL } = selectedFile || {}; const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { const previewURL = URL.createObjectURL(file); setSelectedFile({ file, previewURL }); } }; const handleUpload = async () => { if (selectedFile) { setIsUploading(true); await uploadImage(selectedFile.file); URL.revokeObjectURL(selectedFile.previewURL); setSelectedFile(null); fileInputRef.current?.value && (fileInputRef.current.value = ''); setIsUploading(false); } }; return ( <div className="mb-8 p-5 border-2 border-dashed border-gray-300 rounded-lg"> <input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileSelect} className="font-mono" /> {isUploading ? ( <div className="mt-5 flex flex-col items-center"> <div className="w-8 h-8 border-2 border-t-2 border-gray-200 border-t-green-500 rounded-full animate-spin"></div> <p className="mt-2 text-sm text-gray-600">Uploading...</p> </div> ) : previewURL && ( <div className="mt-5 flex flex-col items-center gap-3"> <img src={previewURL} alt="Preview" className="max-w-xs max-h-xs object-contain" /> <button onClick={handleUpload} className="py-2 px-4 bg-green-500 text-white border-none rounded-sm cursor-pointer font-mono"> Upload Image </button> </div> )} </div> ); } function ImageGrid({ images }: { images: InstantFile[] }) { // Use `db.transact` to delete files const handleDelete = async (image: InstantFile) => { db.transact(db.tx.$files[image.id].delete()); } return ( <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full max-w-6xl"> {images.map((image) => { return ( <div key={image.id} className="border border-gray-300 rounded-lg overflow-hidden"> <div className="relative"> {/* $files entities come with a `url` property */} <img src={image.url} alt={image.path} className="w-full h-64 object-cover" /> </div> <div className="p-3 flex justify-between items-center bg-white"> <span>{image.path}</span> <span onClick={() => handleDelete(image)} className="cursor-pointer text-gray-300 px-1"> 𝘟 </span> </div> </div> ) })} </div> ); } export default App;4. 启动应用
npm run dev访问localhost:3000,即可看到一个支持上传、实时展示与删除的图片墙。打开另一个浏览器标签页,新上传的图片会自动出现——这就是$files查询随数据变更实时更新的体现。
Storage 客户端 SDK 详解
以下从 React 客户端角度详细说明 Storage API 的各个操作。
上传文件
使用db.storage.uploadFile(path, file, opts?)上传文件:
path决定文件在存储中的位置,同时可以与权限规则配合,限制对特定目录下文件的访问;file应为File类型,通常来自<input type="file">文件选择框;opts用于设置额外的元数据,如contentType与contentDisposition。
// use the file's current name as the path await db.storage.uploadFile(file.name, file); // or, give the file a custom name const path = `${user.id}/avatar.png`; await db.storage.uploadFile(path, file); // or, set the content type and content disposition const path = `${user.id}/orders/${orderId}.pdf`; await db.storage.uploadFile(path, file, { contentType: 'application/pdf', contentDisposition: `attachment; filename="${orderId}-confirmation.pdf"`, });从底层实现看,uploadFile最终通过PUT请求发送到${apiURI}/storage/upload,请求头中携带app-id、path、authorization: Bearer <refreshToken>,并默认把content-type设为file.type(见 client/packages/core/src/StorageAPI.ts)。因此opts.contentType的默认值实际是'application/octet-stream'或文件的 MIME 类型,而contentDisposition默认是'inline'(即浏览器内联展示),设置为'attachment'或带filename的完整值则会触发下载行为。
上传成功后返回的响应结构为:
type UploadFileResponse = { data: { id: string; }; };其中的文件id可用于后续的删除与关联操作。
覆盖文件
如果上传的path在存储目录中已经存在,会被直接覆盖:
// Uploads a file to 'demo.png' await db.storage.uploadFile('demo.png', file); // Overwrites the file at 'demo.png' await db.storage.uploadFile('demo.png', file);如果不想覆盖文件,需要自行保证每次上传使用唯一的path(例如在路径中加入用户 id 或时间戳)。
查看文件
通过查询$files命名空间获取文件列表。文件实体的核心属性包括:
id:文件的唯一标识;path:文件在存储中的路径;url:可用于直接展示/下载文件的地址(服务端签名生成);content-type与content-disposition:上传时设置的元数据。
// Fetch all files from earliest to latest upload const query = { $files: { $: { order: { serverCreatedAt: 'asc' }, }, }, }); const { isLoading, error, data } = db.useQuery(query);查询结果示例:
console.log(data) { "$files": [ { "id": fileId, "path": "demo.png" // You can use this URL to serve the file "url": "https://instant-storage.s3.amazonaws.com/...", "content-type": "image/png", "content-disposition": "attachment; filename=\"demo.png\"", }, // ... ] }$files与其他命名空间一样支持查询过滤与排序,可以对文件进行条件筛选和按字段排序(InstaQL 查询语法详见 client/www/app/docs/instaql/page.md)。
还可以借助关联(links)把文件与业务实体绑定。例如下面 schema 定义了profiles与$files之间的多对一关联:
// instant.schema.ts // --------------- import { i } from '@instantdb/core'; const _schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.string(), }), $users: i.entity({ email: i.string().unique().indexed(), }), profiles: i.entity({ nickname: i.string(), createdAt: i.date(), }), }, links: { profileUser: { forward: { on: 'profiles', has: 'one', label: '$user' }, reverse: { on: '$users', has: 'one', label: 'profile' }, }, profileUploads: { forward: { on: 'profiles', has: 'many', label: '$files' }, reverse: { on: '$files', has: 'one', label: 'profile' }, }, }, });随后即可查询"某个用户 profile 下的所有文件":
// app/page.tsx // --------------- // Find files associated with a profile const { user } = db.useAuth(); const query = { profiles: { $: { where: {"$user.id": user.id} }, $files: {}, }, }); // Defer until we've fetched the user and then query associated files const { isLoading, error, data } = db.useQuery(user ? query : null);删除文件
使用db.transact删除文件,支持按id、按path(通过lookup唯一属性查找)以及批量删除:
// Delete by id db.transact(db.tx.$files[fileId].delete()); // Delete by path db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete()); // Delete multiple files db.transact(fileIds.map((id) => db.tx.$files[id].delete()));lookup('path', ...)是 InstaML 提供的按唯一属性定位实体的机制——仓库中 client/packages/core/src/instaml.ts 会校验 lookup 必须包含唯一的属性(path恰好声明为unique()),再将其改写为内部实体 id 引用。客户端db.storage.delete虽然仍可用,但已在源码中标记为@deprecated,官方推荐统一使用db.transact删除(见 client/packages/core/src/index.ts)。
更新文件
可以通过db.transact更新文件的path,以及任何你为$files自定义添加的列。例如 schema 中包含自定义列isFavorite:
import { i } from "@instantdb/react"; const _schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), isFavorite: i.boolean().optional() url: i.string(), }), }, });下面的事务将所有documents/my-video-project/下的文件移动到videos/my-video-project/,并标记为收藏:
// Move all files under 'documents/my-video-project/' to 'videos/my-video-project/' and make them favorites const { data } = await db.query({ $files: { $: { where: { path: { $like: 'documents/my-video-project/%' } } } }, }); await db.transact( data.$files.map((file) => db.tx.$files[file.id].update({ path: file.path.replace( 'documents/my-video-project/', 'videos/my-video-project/', ), isFavorite: true, }), ), );需要注意两点:
path是唯一属性,如果目标path已被其他文件占用,事务会失败;- 目前只允许更新
$files的path属性和自定义列;尝试更新content-type之类的系统属性会导致事务失败。
链接文件
上传成功后,uploadFile返回的data对象中包含该文件的 id,可用来把文件与其他命名空间实体建立关联。下面实现一个"用户头像"上传场景——上传后把文件链接到对应 profile:
async function uploadImage(file: File) { try { const path = `${user.id}/avatar`; const { data } = await db.storage.uploadFile(path, file); await db.transact(db.tx.profiles[profileId].link({ avatar: data.id })); } catch (error) { console.error('Error uploading image:', error); } }仓库中的sandbox/react-nextjs、examples等示例工程均基于@instantdb/react构建,可参考其中的 schema 与事务写法来扩展这类"文件 + 业务实体"的关联模型。
在 React Native 中使用 Storage
db.storage.uploadFile期望传入File或Blob。根据 Expo SDK 版本不同,获取方式有所差异:
- Expo SDK 56 及以上:
expo/fetch作为全局fetch,可直接读取本地文件,因此传入expo-file-system提供的File:
import { File } from 'expo-file-system'; const localFilePath = 'file:///var/mobile/Containers/Data/my_file.m4a'; const file = new File(localFilePath); await db.storage.uploadFile('my_file.m4a', file, { contentType: 'audio/x-m4a', });- Expo SDK 55 及以下(或裸 React Native):内置
fetch返回原生Blob,可包装成File再上传:
const localFilePath = 'file:///var/mobile/Containers/Data/my_file.m4a'; const res = await fetch(localFilePath); const blob = await res.blob(); const file = new File([blob], 'my_file.m4a', { type: 'audio/x-m4a' }); await db.storage.uploadFile('my_file.m4a', file);Storage Admin SDK(服务端)
Admin SDK 提供了与服务端管理场景匹配的存储 API。与客户端 SDK 不同,Admin SDK 不执行权限校验,因此可以绕过认证直接在服务端管理文件——这也意味着使用时要格外注意只在自己的受信后端调用。
上传文件
服务端同样调用db.storage.uploadFile(path, file, opts?),但file参数必须是Buffer(缓冲区)或流(Stream):
import fs from 'fs'; const fp = 'path/to/your/file.png'; const dest = 'images/demo.png'; // Upload a file from a buffer const buffer = fs.readFileSync(filepath); const { data } = await db.storage.uploadFile(dest, buffer); // Upload a file from a stream // IMPORTANT: You must provide `fileSize` as an option when uploading via stream const stream = fs.createReadStream(fp); const fileSize = fs.statSync(fp).size; const { data } = await db.storage.uploadFile(dest, stream, { contentType: contentType, fileSize, });从实现看(client/packages/admin/src/index.ts),Admin SDK 的上传请求发送到${apiURI}/admin/storage/upload?app_id=...。当检测到file是 Node 可读流或 WebReadableStream时,必须提供fileSize,否则会直接抛出'fileSize is required in metadata when uploading streams'错误;内部会把fileSize写入content-length请求头,并为 Node 流设置duplex: 'half'(单向流)。
查看文件
与客户端类似,但使用db.query()而非db.useQuery()(无 React Hooks 环境):
const query = { $files: { $: { order: { serverCreatedAt: 'asc' }, }, }, }); const data = db.query(query);删除文件
同样通过db.transact完成:
// Delete by id await db.transact(db.tx.$files[fileId].delete()); // Delete by path await db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete()); // Delete multiple files await db.transact(fileIds.map((id) => db.tx.$files[id].delete()));链接文件
服务端也可以用上传返回的文件 id 建立关联:
// Assume we have a user ID and a buffer for the file const { data } = await db.storage.uploadFile('images/demo.png', buffer); db.transact([db.tx.$users[userId].link({ avatar: data.id })]);Storage 权限控制
默认情况下 Storage 权限是关闭的:在显式配置权限之前,任何上传与下载都不会成功。各权限关键字的作用如下:
create权限:允许上传$files;view权限:允许查看$files;update权限:允许更新$files;delete权限:允许删除$files;- 对
$files的view权限,加上对正向实体的update权限,才能对$files进行链接(link)与取消链接(unlink)。
在权限规则中可以使用auth访问当前认证用户,使用data访问文件元数据。目前可用的文件元数据仅有data.path,即文件在 Storage 中的路径。
以下是一些典型的权限配置:
允许任何人上传与查看文件(便于快速尝试,不推荐用于生产):
{ "$files": { "allow": { "view": "true", "create": "true" } } }仅允许已登录用户查看与上传:
{ "$files": { "allow": { "view": "isLoggedIn", "create": "isLoggedIn" }, "bind": ["isLoggedIn", "auth.id != null"] } }仅允许用户在自己的子目录内上传、查看与更新(利用data.path前缀校验):
{ "$files": { "allow": { "view": "isOwner", "update": "isOwner", "create": "isOwner" }, "bind": ["isOwner", "data.path.startsWith(auth.id + '/')"] } }最后一条规则是典型的"按用户隔离存储"方案:每个用户上传时把path前缀设为auth.id(例如uploadFile(\${user.id}/avatar.png`, file)),配合data.path.startsWith(auth.id + '/')` 即可确保用户只能访问自己的文件。权限规则的完整语法可进一步参考 client/www/app/docs/permissions/page.md。
小结
Instant Storage 将文件存储纳入了与普通数据一致的查询与权限体系:$files命名空间让文件像实体一样可查询、排序、关联与实时同步;db.storage.uploadFile一行代码完成上传;db.transact统一处理删除、更新与关联;权限规则基于auth与data.path实现细粒度的访问控制。无论是 Next.js / React 客户端、React Native 移动端,还是需要绕过权限校验的服务端 Admin SDK,Instant Storage 都提供了对应的一体化 API,让你专注业务本身而非存储基础设施。
- 后端
- 数据库
【免费下载链接】instant
Instant is the best backend for AI-coded apps. You get auth, permissions, storage, presence, and streams — everything you need to ship apps your users will love.
相关推荐
零信任防护:CodeIgniter文件上传与权限控制实战指南
零信任防护:CodeIgniter文件上传与权限控制实战指南 在Web应用开发中,文件上传功能是最常见的攻击入口之一。作为一款轻量级PHP框架,CodeIgni
后端Web框架VRCX终极指南:如何用这款免费工具让VRChat社交管理效率提升300%
VRCX终极指南:如何用这款免费工具让VRChat社交管理效率提升300% VRCX是一款专门为VRChat玩家设计的免费社交管理工具,它通过智能化的好友关系管
桌面应用突破上传限制:lowcode-engine大文件分片上传终极实战指南
突破上传限制:lowcode engine大文件分片上传终极实战指南 在低代码开发领域, lowcode engine 作为一套面向扩展设计的企业级低代码技术体
前端低代码
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考