ReboFilesDocs
Empezar3
Qué es ReboFiles
Primeros pasos
Demos para ejecutar
Ponlo en tu app6
Componente de React
Headless: tu propia interfaz
Sin framework
Estilos y temas
Teclado y accesibilidad
Idiomas
Archivos9
Subidas
Vistas previas y el visor
Búsqueda
Cambios en vivo y carpetas grandes
Comentarios
Destacados y campos
Versiones, papelera y espacio
Descargar como .zip
Documentos de Word
El servidor14
La ruta
Frameworks e inicio de sesión
Quién puede hacer qué
Almacenamiento: S3, GCS, Azure y disco
Revisar las subidas
Metadatos: SQLite y Postgres
Esquema de la base de datos
Compartir y enlaces
Actividad y notificaciones
Ganchos, registros y límites
Retención, bloqueos y exportación
Escritorio y otros orígenes
Sincronizar una carpeta
Ir a producción
En equipo1
Asistentes de IA (MCP)
Referencia5
Paquetes
Componentes y hooks
API del cliente
Rutas HTTP
Estado y hoja de ruta
↑↓ para moverte↵ para abrirEsc para cerrar38 páginas

El servidor Frameworks e inicio de sesión

Frameworks e inicio de sesión

createDriveHandler recibe un Request y da un Response, así que cada framework necesita unas pocas líneas: pasarle la petición tal como llegó, antes de que nada lea su cuerpo. Cada ejemplo aquí es un archivo de examples/, servido en un puerto y consultado por HTTP en sus tests: una carpeta creada, un archivo de dos partes subido y leído de vuelta, y un cambio avisado en el momento.
Base

Un drive para todos

Los bytes en un bucket cuando S3_BUCKET lo indica, si no en el disco; las entradas en SQLite. Igual en cada ejemplo.

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

Un route handler para todas las rutas, en Node: firma con 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

Montado antes de express.json(): el drive lee sus propios cuerpos.

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

La petición de Hono es la que recibe el drive.

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

En un plugin propio, donde ningún parser de Fastify lee el cuerpo, y la respuesta escrita en la respuesta cruda.

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');
}
Sesión

Quién pregunta, con 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;
}

Pásalo como identify. La página envía la cookie de sesión con cada petición a su propio origen, así que en el navegador no hace falta nada más.

Sesión

Quién pregunta, con 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

Un token propio

Para una aplicación con su propio inicio de sesión, y para la aplicación de escritorio, que envía `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;
  }
}

Probado, y no: el token propio lo comprueban los tests, y uno falsificado se rechaza. Auth.js y Clerk están escritos según su documentación y no se ejecutan aquí, porque necesitan cuentas suyas.

Errores

Cuando algo falla

The body is not JSON.

Un parser del framework leyó el cuerpo primero: monta el drive antes, o fuera de su alcance, como hacen los ejemplos.

No GET /api/files/…

La ruta que dio el framework no es la completa. nodeHandler toma originalUrl cuando lo hay (Express); si no, móntalo donde dice base.

Sign in first.

identify no encontró a nadie: no llegó la cookie (otro origen necesita cors y un token), o la sesión no tiene id.