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>
90 lines
3.2 KiB
JavaScript
90 lines
3.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { basenameOf, joinRel, safeJoin, trashRoot } from './paths.mjs';
|
|
import { removeFileMeta } from './fs-tree.mjs';
|
|
|
|
async function ensureTrashDir(ctx) {
|
|
await fsp.mkdir(trashRoot(ctx), { recursive: true });
|
|
}
|
|
|
|
function trashNameFor(rel) {
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
return `${stamp}__${rel.replace(/\//g, '__')}`;
|
|
}
|
|
|
|
export async function moveToTrash(ctx, rel) {
|
|
await ensureTrashDir(ctx);
|
|
const src = safeJoin(ctx.uploadDir, rel);
|
|
if (!fs.existsSync(src)) throw new Error('not found');
|
|
const entryName = trashNameFor(rel);
|
|
const dest = path.join(trashRoot(ctx), entryName);
|
|
await fsp.rename(src, dest);
|
|
await removeFileMeta(ctx, rel);
|
|
return {
|
|
id: entryName,
|
|
original: rel,
|
|
trashedAt: Date.now(),
|
|
};
|
|
}
|
|
|
|
export async function listTrash(ctx) {
|
|
await ensureTrashDir(ctx);
|
|
const entries = await fsp.readdir(trashRoot(ctx), { withFileTypes: true });
|
|
const items = [];
|
|
for (const entry of entries) {
|
|
const full = path.join(trashRoot(ctx), entry.name);
|
|
const stat = await fsp.stat(full);
|
|
const match = entry.name.match(/^(.+)__(.+)$/);
|
|
items.push({
|
|
id: entry.name,
|
|
original: match ? match[2].replace(/__/g, '/') : entry.name,
|
|
trashedAt: stat.mtimeMs,
|
|
isDirectory: stat.isDirectory(),
|
|
size: stat.isDirectory() ? null : stat.size,
|
|
});
|
|
}
|
|
return items.sort((a, b) => b.trashedAt - a.trashedAt);
|
|
}
|
|
|
|
export async function restoreFromTrash(ctx, id, onConflict = 'rename') {
|
|
const src = path.join(trashRoot(ctx), id);
|
|
if (!fs.existsSync(src)) throw new Error('not found');
|
|
const item = (await listTrash(ctx)).find((x) => x.id === id);
|
|
if (!item) throw new Error('not found');
|
|
let dest = safeJoin(ctx.uploadDir, item.original);
|
|
if (fs.existsSync(dest)) {
|
|
if (onConflict === 'skip') return null;
|
|
if (onConflict === 'overwrite') await fsp.rm(dest, { recursive: true, force: true });
|
|
else {
|
|
const base = basenameOf(item.original);
|
|
const parent = item.original.includes('/') ? item.original.slice(0, item.original.lastIndexOf('/')) : '';
|
|
let i = 1;
|
|
let candidate = joinRel(parent, `${path.parse(base).name} (恢复)${path.extname(base)}`);
|
|
while (fs.existsSync(safeJoin(ctx.uploadDir, candidate))) {
|
|
i += 1;
|
|
candidate = joinRel(parent, `${path.parse(base).name} (恢复 ${i})${path.extname(base)}`);
|
|
}
|
|
dest = safeJoin(ctx.uploadDir, candidate);
|
|
}
|
|
}
|
|
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
|
await fsp.rename(src, dest);
|
|
const finalRel = path.relative(ctx.uploadDir, dest).split(path.sep).join('/');
|
|
return { id, from: item.original, to: finalRel };
|
|
}
|
|
|
|
export async function emptyTrash(ctx) {
|
|
await ensureTrashDir(ctx);
|
|
const entries = await fsp.readdir(trashRoot(ctx), { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
await fsp.rm(path.join(trashRoot(ctx), entry.name), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
export async function purgeTrashItem(ctx, id) {
|
|
const target = path.join(trashRoot(ctx), id);
|
|
if (!fs.existsSync(target)) throw new Error('not found');
|
|
await fsp.rm(target, { recursive: true, force: true });
|
|
}
|