服务器 框架与登录
框架与登录
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,
});
}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 };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'));
}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'));
}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;
}自己的令牌
适用于有自己登录系统的应用,以及发送 `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。