Files
memind/mindspace-oa-drive/drive-runtime.mjs
T
john 6d5eb15b1b feat(mindspace): add per-user OA cloud drive with fds-style file manager
Expose authenticated /api/mindspace/v1/oa/drive APIs over each user's oa/ workspace and wire a MindSpace entry at /space/oa/drive.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:55:46 +08:00

91 lines
2.2 KiB
JavaScript

import fs from 'node:fs';
import { enrichFiles, walkUploadDir } from './lib/fs-tree.mjs';
export function createDriveRuntime(ctx) {
const state = {
ctx,
listCache: { files: [], dirs: [], ts: 0, added: [], removed: [] },
sseClients: new Set(),
watchTimer: null,
watcher: null,
};
async function refreshFileList() {
const { files, dirs } = await walkUploadDir(ctx, ctx.uploadDir);
const prevNames = new Set(state.listCache.files.map((f) => f.name));
const added = files.filter((f) => !prevNames.has(f.name)).map((f) => f.name);
const nextNames = new Set(files.map((f) => f.name));
const removed = state.listCache.files.filter((f) => !nextNames.has(f.name)).map((f) => f.name);
const enriched = await enrichFiles(ctx, files);
state.listCache = {
files: enriched,
dirs,
ts: Date.now(),
added,
removed,
delta: added.length > 0 || removed.length > 0,
};
return state.listCache;
}
function broadcastList(payload) {
const body = `data: ${JSON.stringify(payload)}\n\n`;
for (const client of state.sseClients) {
try {
client.write(body);
} catch {
state.sseClients.delete(client);
}
}
}
async function notifyChange() {
const payload = await refreshFileList();
broadcastList(payload);
return payload;
}
function scheduleNotify() {
clearTimeout(state.watchTimer);
state.watchTimer = setTimeout(() => {
notifyChange().catch(console.error);
}, 250);
}
function initWatcher() {
if (state.watcher) return;
try {
state.watcher = fs.watch(ctx.uploadDir, { recursive: true }, scheduleNotify);
} catch (err) {
console.warn('[oa-drive] fs.watch unavailable:', err.message);
}
}
async function bootstrap() {
await refreshFileList();
initWatcher();
}
function dispose() {
clearTimeout(state.watchTimer);
if (state.watcher) {
state.watcher.close();
state.watcher = null;
}
state.sseClients.clear();
}
return {
ctx,
getListCache: () => state.listCache,
refreshFileList,
notifyChange,
addSseClient(res) {
state.sseClients.add(res);
return () => state.sseClients.delete(res);
},
bootstrap,
dispose,
};
}