This repository has been archived on 2026-03-27. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drive/server/migrate.ts
Sienna Meridian Satterwhite 58237d9e44 Initial commit — Drive, an S3 file browser with WOPI editing
Lightweight replacement for the upstream La Suite Numérique drive
(Django/Celery/Next.js) built as a single Deno binary.

Server (Deno + Hono):
- S3 file operations via AWS SigV4 (no SDK) with pre-signed URLs
- WOPI host for Collabora Online (CheckFileInfo, GetFile, PutFile, locks)
- Ory Kratos session auth + CSRF protection
- Ory Keto permission model (OPL namespaces, not yet wired to routes)
- PostgreSQL metadata with recursive folder sizes
- S3 backfill API for registering files uploaded outside the UI
- OpenTelemetry tracing + metrics (opt-in via OTEL_ENABLED)

Frontend (React 19 + Cunningham v4 + react-aria):
- File browser with GridList, keyboard nav, multi-select
- Collabora editor iframe (full-screen, form POST, postMessage)
- Profile menu, waffle menu, drag-drop upload, asset type badges
- La Suite integration service theming (runtime CSS)

Testing (549 tests):
- 235 server unit tests (Deno) — 90%+ coverage
- 278 UI unit tests (Vitest) — 90%+ coverage
- 11 E2E tests (Playwright)
- 12 integration service tests (Playwright)
- 13 WOPI integration tests (Playwright + Docker Compose + Collabora)

MIT licensed.
2026-03-25 18:28:37 +00:00

117 lines
3.7 KiB
TypeScript

import sql from "./db.ts";
const MIGRATIONS = [
{
name: "001_create_files",
up: `
CREATE TABLE IF NOT EXISTS files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
s3_key TEXT NOT NULL UNIQUE,
filename TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT 'application/octet-stream',
size BIGINT NOT NULL DEFAULT 0,
owner_id TEXT NOT NULL,
parent_id UUID REFERENCES files(id) ON DELETE CASCADE,
is_folder BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_files_parent ON files(parent_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_files_owner ON files(owner_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_files_s3key ON files(s3_key);
`,
},
{
name: "002_create_user_file_state",
up: `
CREATE TABLE IF NOT EXISTS user_file_state (
user_id TEXT NOT NULL,
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
favorited BOOLEAN NOT NULL DEFAULT false,
last_opened TIMESTAMPTZ,
PRIMARY KEY (user_id, file_id)
);
`,
},
{
name: "004_folder_sizes",
up: `
-- Recompute a single folder's size from its direct + nested descendants.
-- Called after any file size change, create, delete, or move.
CREATE OR REPLACE FUNCTION recompute_folder_size(folder_id UUID)
RETURNS BIGINT LANGUAGE SQL AS $$
WITH RECURSIVE descendants AS (
-- Direct children of this folder
SELECT id, size, is_folder
FROM files
WHERE parent_id = folder_id AND deleted_at IS NULL
UNION ALL
-- Recurse into subfolders
SELECT f.id, f.size, f.is_folder
FROM files f
JOIN descendants d ON f.parent_id = d.id
WHERE f.deleted_at IS NULL
)
SELECT COALESCE(SUM(size) FILTER (WHERE NOT is_folder), 0)
FROM descendants;
$$;
-- Propagate size updates from a file up through all ancestor folders.
CREATE OR REPLACE FUNCTION propagate_folder_sizes(start_parent_id UUID)
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE
current_id UUID := start_parent_id;
computed BIGINT;
BEGIN
WHILE current_id IS NOT NULL LOOP
computed := recompute_folder_size(current_id);
UPDATE files SET size = computed WHERE id = current_id AND is_folder = true;
SELECT parent_id INTO current_id FROM files WHERE id = current_id;
END LOOP;
END;
$$;
`,
},
{
name: "003_create_migrations_table",
up: `
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`,
},
];
async function migrate() {
// Ensure migrations table exists first
await sql.unsafe(`
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`);
for (const migration of MIGRATIONS) {
const [existing] = await sql`
SELECT name FROM _migrations WHERE name = ${migration.name}
`;
if (existing) {
console.log(` skip: ${migration.name}`);
continue;
}
console.log(` apply: ${migration.name}`);
await sql.unsafe(migration.up);
await sql`INSERT INTO _migrations (name) VALUES (${migration.name})`;
}
console.log("Migrations complete.");
}
if (import.meta.main) {
await migrate();
await sql.end();
}
export { migrate };