6d5eb15b1b
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>
91 lines
2.2 KiB
JavaScript
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,
|
|
};
|
|
}
|