ReboFiles文档
开始3
什么是 ReboFiles
快速开始
可运行的演示
集成到你的应用6
React 组件
Headless:你自己的界面
不使用框架
样式与主题
键盘与无障碍
语言
文件9
上传
预览与查看器
搜索
实时更改与大文件夹
评论
星标与字段
版本、回收站与空间
打包下载 .zip
Word 文档
服务器14
路由
框架与登录
谁可以做什么
存储:S3、GCS、Azure 与磁盘
检查上传
元数据:SQLite 与 Postgres
数据库结构
共享与链接
动态与通知
钩子、日志与限制
保留、法律保留与导出
桌面端与其他来源
同步文件夹
上线生产环境
协作1
AI 助手(MCP)
参考5
包
组件与 hooks
客户端 API
HTTP 路由
状态与路线图
↑↓ 移动↵ 打开Esc 关闭38 个页面

服务器 框架与登录

框架与登录

createDriveHandler 接收 Request 并返回 Response,因此每个框架只需几行代码:在任何东西读取请求体之前,把请求原样交给它。这里的每个示例都是 examples/ 中的文件,测试会在一个端口上启动它并通过 HTTP 访问:创建文件夹、上传并读回一个分两部分的文件,以及实时收到一次更改。
基础

所有示例共用的网盘

设置了 S3_BUCKET 时字节存入存储桶,否则存在磁盘上;条目存在 SQLite 中。

examples/drive.ts
import { mkdirSync } from 'node:fs';
import { type DriveServerOptions, byRoles, createDriveHandler, diskStorage, s3OptionsFrom, s3Storage, sqliteMetadata } from '@rebofiles/server';

/** Long and random, the same in every process: `openssl rand -hex 32`. */
const secret = process.env.REBOFILES_SECRET ?? (process.env.NODE_ENV === 'production' ? undefined : 'a secret for this computer only');
if (!secret) throw new Error('Set REBOFILES_SECRET: long, random, the same in every process.');

export async function createDrive(identify: DriveServerOptions['identify'], extra: Partial<DriveServerOptions> = {}) {
  const folder = process.env.REBOFILES_FOLDER ?? './storage';
  mkdirSync(folder, { recursive: true });
  return createDriveHandler({
    storage: process.env.S3_BUCKET ? s3Storage(s3OptionsFrom()) : diskStorage({ folder: `${folder}/files`, secret: secret! }),
    metadata: await sqliteMetadata(`${folder}/drive.sqlite`),
    identify,
    authorize: byRoles,
    secret: secret!,
    base: '/api/files',
    ...extra,
  });
}
App Router

Next.js

一个路由处理器处理全部路由,运行在 Node 中:签名使用 node:crypto。

app/api/files/[...path]/route.ts
import { createDrive } from '../../../../../drive';
import { identify } from '../../../../../auth/dev';

export const runtime = 'nodejs';
// Never cached: every answer is of the one asking, and the events go on as long as the page watches.
export const dynamic = 'force-dynamic';

const handle = await createDrive(identify);

export { handle as GET, handle as POST, handle as PUT, handle as PATCH, handle as DELETE, handle as OPTIONS };
Node

Express

挂载在 express.json() 之前:网盘自己读取请求体。

examples/express/server.ts
import express from 'express';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const app = express();
  app.use('/api/files', nodeHandler(await createDrive(identify)));
  // The rest of your application, its parsers among it, after.
  app.use(express.json());
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const app = await createApp();
  app.listen(3000, () => console.log('The drive is at http://localhost:3000/api/files'));
}
Node · Bun · Deno

Hono

Hono 的请求就是网盘接收的请求。

examples/hono/server.ts
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const handle = await createDrive(identify);
  const app = new Hono();
  app.all('/api/files/*', (c) => handle(c.req.raw));
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  serve({ fetch: (await createApp()).fetch, port: 3000 }, () => console.log('The drive is at http://localhost:3000/api/files'));
}
Node

Fastify

放在独立的插件中,Fastify 的解析器不会读取请求体,响应直接写入原始响应。

examples/fastify/server.ts
import Fastify from 'fastify';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const drive = nodeHandler(await createDrive(identify));
  const app = Fastify();
  await app.register(async (scope) => {
    // Every body left as it came, in this plugin only: the drive reads it.
    scope.removeAllContentTypeParsers();
    scope.addContentTypeParser('*', (_request, _payload, done) => done(null));
    scope.all('/api/files/*', (request, reply) => {
      reply.hijack();
      return drive(request.raw, reply.raw);
    });
    scope.all('/api/files', (request, reply) => {
      reply.hijack();
      return drive(request.raw, reply.raw);
    });
  });
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const app = await createApp();
  await app.listen({ port: 3000 });
  console.log('The drive is at http://localhost:3000/api/files');
}
登录

用 Auth.js 识别用户

examples/auth/authjs.ts
import type { User } from '@rebofiles/server';
import { auth } from '@/auth';

export async function identify(): Promise<User | null> {
  const session = await auth();
  const id = session?.user?.id;
  return id ? { id, admin: session.user?.email === process.env.DRIVE_ADMIN } : null;
}

将它作为 identify 传入。页面向自己的源发出的每个请求都会带上会话 cookie,浏览器端无需其他设置。

登录

用 Clerk 识别用户

examples/auth/clerk.ts
import { createClerkClient } from '@clerk/backend';
import type { User } from '@rebofiles/server';

const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.CLERK_PUBLISHABLE_KEY });

export async function identify(request: Request): Promise<User | null> {
  const state = await clerk.authenticateRequest(request, { authorizedParties: [process.env.APP_ORIGIN ?? 'http://localhost:3000'] });
  const { userId, orgRole } = state.toAuth() ?? {};
  return userId ? { id: userId, admin: orgRole === 'org:admin' } : null;
}
JWT

自己的令牌

适用于有自己登录系统的应用,以及发送 `Authorization: Bearer` 的桌面应用。

examples/auth/jwt.ts
import type { User } from '@rebofiles/server';
import { jwtVerify } from 'jose';

const key = new TextEncoder().encode(process.env.SESSION_SECRET);

export async function identify(request: Request): Promise<User | null> {
  const bearer = /^Bearer (.+)$/.exec(request.headers.get('authorization') ?? '')?.[1];
  const cookie = /(?:^|;\s*)session=([^;]+)/.exec(request.headers.get('cookie') ?? '')?.[1];
  const token = bearer ?? cookie;
  if (!token) return null;
  try {
    const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] });
    return payload.sub ? { id: payload.sub, admin: payload.role === 'admin' } : null;
  } catch {
    return null;
  }
}

已测试与未测试: 自己的令牌由测试检查,伪造的令牌会被拒绝。Auth.js 和 Clerk 按其文档编写,未在此运行,因为它们需要各自的账号。

错误

出错时

The body is not JSON.

框架的解析器先读取了请求体:像示例那样,把网盘挂载在它之前或它管不到的地方。

No GET /api/files/…

框架给出的路径不完整。有 originalUrl 时(Express)nodeHandler 会使用它;否则请挂载在 base 所指的位置。

Sign in first.

identify 没有找到用户:cookie 没有送达(其他源需要 cors 和令牌),或会话中没有 id。