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>
130 lines
3.8 KiB
JavaScript
130 lines
3.8 KiB
JavaScript
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { isReservedName, metaFile, safeJoin } from './paths.mjs';
|
|
|
|
const PREVIEWABLE = new Set([
|
|
'.html', '.htm', '.css', '.js', '.mjs', '.json', '.png', '.jpg', '.jpeg', '.gif',
|
|
'.webp', '.svg', '.pdf', '.txt', '.md', '.csv', '.xml', '.mp4', '.webm', '.mp3', '.wav', '.ogg',
|
|
]);
|
|
|
|
export function isPreviewable(filePath) {
|
|
return PREVIEWABLE.has(path.extname(filePath).toLowerCase());
|
|
}
|
|
|
|
export function mimeFor(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const map = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.htm': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.mjs': 'text/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.webp': 'image/webp',
|
|
'.svg': 'image/svg+xml',
|
|
'.pdf': 'application/pdf',
|
|
'.zip': 'application/zip',
|
|
'.txt': 'text/plain; charset=utf-8',
|
|
'.md': 'text/markdown; charset=utf-8',
|
|
'.csv': 'text/csv; charset=utf-8',
|
|
'.xml': 'application/xml; charset=utf-8',
|
|
'.mp4': 'video/mp4',
|
|
'.webm': 'video/webm',
|
|
'.mp3': 'audio/mpeg',
|
|
'.wav': 'audio/wav',
|
|
'.ogg': 'audio/ogg',
|
|
};
|
|
return map[ext] || 'application/octet-stream';
|
|
}
|
|
|
|
export async function walkUploadDir(ctx, dir, base = dir) {
|
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
const files = [];
|
|
const dirs = [];
|
|
for (const entry of entries) {
|
|
if (isReservedName(ctx, entry.name)) continue;
|
|
const full = path.join(dir, entry.name);
|
|
const rel = path.relative(base, full).split(path.sep).join('/');
|
|
if (entry.isDirectory()) {
|
|
dirs.push(rel);
|
|
const nested = await walkUploadDir(ctx, full, base);
|
|
files.push(...nested.files);
|
|
dirs.push(...nested.dirs);
|
|
} else if (entry.isFile()) {
|
|
const stat = await fsp.stat(full);
|
|
files.push({
|
|
name: rel,
|
|
size: stat.size,
|
|
mtime: stat.mtimeMs,
|
|
previewable: isPreviewable(full),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
files: files.sort((a, b) => b.mtime - a.mtime),
|
|
dirs,
|
|
};
|
|
}
|
|
|
|
export async function loadMeta(ctx) {
|
|
const file = metaFile(ctx);
|
|
try {
|
|
const stat = await fsp.stat(file);
|
|
const raw = await fsp.readFile(file, 'utf8');
|
|
return { mtime: stat.mtimeMs, data: JSON.parse(raw) };
|
|
} catch {
|
|
return { mtime: Date.now(), data: { files: {} } };
|
|
}
|
|
}
|
|
|
|
export async function saveMeta(ctx, data) {
|
|
const file = metaFile(ctx);
|
|
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
await fsp.writeFile(file, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
export async function setFileMeta(ctx, relPath, meta) {
|
|
const loaded = await loadMeta(ctx);
|
|
loaded.data.files[relPath] = {
|
|
...(loaded.data.files[relPath] || {}),
|
|
...meta,
|
|
updatedAt: Date.now(),
|
|
};
|
|
await saveMeta(ctx, loaded.data);
|
|
}
|
|
|
|
export async function removeFileMeta(ctx, relPath) {
|
|
const loaded = await loadMeta(ctx);
|
|
delete loaded.data.files[relPath];
|
|
await saveMeta(ctx, loaded.data);
|
|
}
|
|
|
|
export async function renameFileMeta(ctx, fromRel, toRel) {
|
|
const loaded = await loadMeta(ctx);
|
|
if (loaded.data.files[fromRel]) {
|
|
loaded.data.files[toRel] = { ...loaded.data.files[fromRel], updatedAt: Date.now() };
|
|
delete loaded.data.files[fromRel];
|
|
await saveMeta(ctx, loaded.data);
|
|
}
|
|
}
|
|
|
|
export async function enrichFiles(ctx, files) {
|
|
const loaded = await loadMeta(ctx);
|
|
return files.map((f) => ({
|
|
...f,
|
|
origin: loaded.data.files[f.name] || null,
|
|
}));
|
|
}
|
|
|
|
export function statEntry(ctx, rel) {
|
|
const full = safeJoin(ctx.uploadDir, rel);
|
|
if (!fs.existsSync(full)) return null;
|
|
const stat = fs.statSync(full);
|
|
return { full, stat, isDirectory: stat.isDirectory() };
|
|
}
|