/workspaces refactor
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { createRouter } from '../../create-router';
|
||||
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile } from '@@/data-path';
|
||||
|
||||
export const workspacesRouter = createRouter();
|
||||
|
||||
type KeyMapping = { file: string; dir?: string };
|
||||
|
||||
type ResolveDirs = { wsDir: string; homepageDir: string };
|
||||
|
||||
function workspaceDir(dirs: ResolveDirs, id: string) {
|
||||
return id === 'ws-homepage' ? dirs.homepageDir : join(dirs.wsDir, id);
|
||||
}
|
||||
|
||||
function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
|
||||
if (key === 'workspaces') return { file: join(dirs.wsDir, 'index.json') };
|
||||
if (key === 'ws-terminals-default') return { file: join(dirs.wsDir, 'default-terminals.json') };
|
||||
if (key === 'ws-host-terminals-default') return { file: join(dirs.wsDir, 'default-host-terminals.json') };
|
||||
|
||||
const layoutMatch = key.match(/^ws-layout-(.+)$/);
|
||||
if (layoutMatch) {
|
||||
const id = layoutMatch[1]!;
|
||||
const dir = workspaceDir(dirs, id);
|
||||
return { file: join(dir, 'layout.json'), dir };
|
||||
}
|
||||
|
||||
const terminalsMatch = key.match(/^ws-terminals-(.+)$/);
|
||||
if (terminalsMatch) {
|
||||
const id = terminalsMatch[1]!;
|
||||
const dir = workspaceDir(dirs, id);
|
||||
return { file: join(dir, 'terminals.json'), dir };
|
||||
}
|
||||
|
||||
const hostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/);
|
||||
if (hostTerminalsMatch) {
|
||||
const id = hostTerminalsMatch[1]!;
|
||||
const dir = workspaceDir(dirs, id);
|
||||
return { file: join(dir, 'host-terminals.json'), dir };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readJsonFile(path: string): Promise<unknown | null> {
|
||||
const file = Bun.file(path);
|
||||
if (await file.exists()) return file.json();
|
||||
return null;
|
||||
}
|
||||
|
||||
async function writeJsonFile(path: string, data: unknown) {
|
||||
await Bun.write(path, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
async function migrateFromState(email: string, dirs: ResolveDirs) {
|
||||
const stateFile = getUserStateFile(email);
|
||||
const file = Bun.file(stateFile);
|
||||
if (!(await file.exists())) return;
|
||||
|
||||
const state = (await file.json()) as Record<string, unknown>;
|
||||
const wsKeys = Object.keys(state).filter(
|
||||
(k) => k === 'workspaces' || k.startsWith('ws-layout-') || k.startsWith('ws-terminals-') || k.startsWith('ws-host-terminals-'),
|
||||
);
|
||||
if (wsKeys.length === 0) return;
|
||||
|
||||
await mkdir(dirs.wsDir, { recursive: true });
|
||||
|
||||
for (const key of wsKeys) {
|
||||
const migratedKey = key === 'ws-layout-workspaces' ? 'ws-layout-ws-homepage' : key;
|
||||
const mapping = resolveKey(dirs, migratedKey);
|
||||
if (!mapping) continue;
|
||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
||||
await writeJsonFile(mapping.file, state[key]);
|
||||
}
|
||||
|
||||
const cleaned = { ...state };
|
||||
for (const key of wsKeys) delete cleaned[key];
|
||||
await Bun.write(stateFile, JSON.stringify(cleaned, null, 2));
|
||||
}
|
||||
|
||||
async function migrateListLayout(dirs: ResolveDirs) {
|
||||
const oldFile = join(dirs.wsDir, 'list-layout.json');
|
||||
if (!(await Bun.file(oldFile).exists())) return;
|
||||
await mkdir(dirs.homepageDir, { recursive: true });
|
||||
await rename(oldFile, join(dirs.homepageDir, 'layout.json'));
|
||||
}
|
||||
|
||||
async function migrateHomepageFromWorkspaces(dirs: ResolveDirs) {
|
||||
const oldDir = join(dirs.wsDir, 'ws-homepage');
|
||||
const layoutFile = join(oldDir, 'layout.json');
|
||||
if (!(await Bun.file(layoutFile).exists())) return;
|
||||
await mkdir(dirs.homepageDir, { recursive: true });
|
||||
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
|
||||
const src = join(oldDir, name);
|
||||
if (await Bun.file(src).exists()) {
|
||||
await rename(src, join(dirs.homepageDir, name));
|
||||
}
|
||||
}
|
||||
const remaining = await readdir(oldDir);
|
||||
if (remaining.length === 0) await rm(oldDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function readWorkspaceDir(dirPath: string, id: string, result: Record<string, unknown>) {
|
||||
const layout = await readJsonFile(join(dirPath, 'layout.json'));
|
||||
if (layout !== null) result[`ws-layout-${id}`] = layout;
|
||||
|
||||
const terminals = await readJsonFile(join(dirPath, 'terminals.json'));
|
||||
if (terminals !== null) result[`ws-terminals-${id}`] = terminals;
|
||||
|
||||
const hostTerminals = await readJsonFile(join(dirPath, 'host-terminals.json'));
|
||||
if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals;
|
||||
}
|
||||
|
||||
async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string, unknown>> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
const indexData = await readJsonFile(join(dirs.wsDir, 'index.json'));
|
||||
if (indexData !== null) result['workspaces'] = indexData;
|
||||
|
||||
const defaultTerminals = await readJsonFile(join(dirs.wsDir, 'default-terminals.json'));
|
||||
if (defaultTerminals !== null) result['ws-terminals-default'] = defaultTerminals;
|
||||
|
||||
const defaultHostTerminals = await readJsonFile(join(dirs.wsDir, 'default-host-terminals.json'));
|
||||
if (defaultHostTerminals !== null) result['ws-host-terminals-default'] = defaultHostTerminals;
|
||||
|
||||
// Read ws-homepage from its own dir
|
||||
await readWorkspaceDir(dirs.homepageDir, 'ws-homepage', result);
|
||||
|
||||
// Read per-workspace subdirs
|
||||
let entries: import('node:fs').Dirent[] = [];
|
||||
try {
|
||||
entries = await readdir(dirs.wsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
await readWorkspaceDir(join(dirs.wsDir, entry.name), entry.name, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDirs(email: string): ResolveDirs {
|
||||
return { wsDir: getUserWorkspacesDir(email), homepageDir: getUserHomepageWorkspaceDir(email) };
|
||||
}
|
||||
|
||||
// GET /workspaces-state
|
||||
workspacesRouter.get('/workspaces-state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const dirs = getDirs(email);
|
||||
|
||||
const dirFile = Bun.file(join(dirs.wsDir, 'index.json'));
|
||||
if (!(await dirFile.exists())) {
|
||||
await migrateFromState(email, dirs);
|
||||
}
|
||||
|
||||
await migrateListLayout(dirs);
|
||||
await migrateHomepageFromWorkspaces(dirs);
|
||||
|
||||
const state = await readAllWorkspacesState(dirs);
|
||||
return ctx.json(state);
|
||||
});
|
||||
|
||||
// PATCH /workspaces-state
|
||||
workspacesRouter.patch('/workspaces-state', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const body = ctx.get('body') as Record<string, unknown>;
|
||||
const dirs = getDirs(email);
|
||||
|
||||
await mkdir(dirs.wsDir, { recursive: true });
|
||||
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
const mapping = resolveKey(dirs, key);
|
||||
if (!mapping) continue;
|
||||
|
||||
if (value === null) {
|
||||
try {
|
||||
await rm(mapping.file, { force: true });
|
||||
if (mapping.dir) {
|
||||
const remaining = await readdir(mapping.dir);
|
||||
if (remaining.length === 0) await rm(mapping.dir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
||||
await writeJsonFile(mapping.file, value);
|
||||
}
|
||||
|
||||
const state = await readAllWorkspacesState(dirs);
|
||||
return ctx.json(state);
|
||||
});
|
||||
Reference in New Issue
Block a user