Files
memind/mindspace-oa-drive/lib/trash.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

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 });
}