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 separator = entry.name.indexOf('__'); const original = separator >= 0 ? entry.name.slice(separator + 2).replace(/__/g, '/') : entry.name; items.push({ id: entry.name, original, 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 }); }