import fs from 'node:fs'; import fsp from 'node:fs/promises'; import path from 'node:path'; import { basenameOf, joinRel, safeJoin } from './paths.mjs'; export function resolveConflictPath(destPath, mode = 'rename') { if (!fs.existsSync(destPath)) return destPath; if (mode === 'skip') return null; if (mode === 'overwrite') return destPath; const dir = path.dirname(destPath); const ext = path.extname(destPath); const base = path.basename(destPath, ext); let i = 1; while (fs.existsSync(path.join(dir, `${base} (${i})${ext}`))) i += 1; return path.join(dir, `${base} (${i})${ext}`); } export async function ensureParentDir(destPath) { await fsp.mkdir(path.dirname(destPath), { recursive: true }); } export async function copyEntryRecursive(src, dest) { const stat = await fsp.stat(src); if (stat.isDirectory()) { await fsp.mkdir(dest, { recursive: true }); const entries = await fsp.readdir(src, { withFileTypes: true }); for (const entry of entries) { await copyEntryRecursive(path.join(src, entry.name), path.join(dest, entry.name)); } return; } await ensureParentDir(dest); await fsp.copyFile(src, dest); } export async function transferEntry({ uploadDir, fromRel, destFolder, mode = 'move', onConflict = 'rename' }) { const src = safeJoin(uploadDir, fromRel); if (!fs.existsSync(src)) throw new Error('not found'); const base = basenameOf(fromRel); const toRel = joinRel(destFolder, base); let dest = safeJoin(uploadDir, toRel); const srcResolved = path.resolve(src); if (destFolder === fromRel || dest.startsWith(srcResolved + path.sep)) throw new Error('invalid move'); const resolved = resolveConflictPath(dest, fs.existsSync(dest) ? onConflict : 'rename'); if (!resolved) return null; dest = resolved; const finalRel = path.relative(uploadDir, dest).split(path.sep).join('/'); if (fs.existsSync(dest) && onConflict === 'overwrite') { const stat = await fsp.stat(dest); if (stat.isDirectory()) await fsp.rm(dest, { recursive: true, force: true }); else await fsp.unlink(dest); } await ensureParentDir(dest); if (mode === 'copy') { await copyEntryRecursive(src, dest); } else { await fsp.rename(src, dest); } return { from: fromRel, to: finalRel }; } export async function renameEntry({ uploadDir, fromRel, newName, onConflict = 'rename' }) { const src = safeJoin(uploadDir, fromRel); if (!fs.existsSync(src)) throw new Error('not found'); const parent = fromRel.includes('/') ? fromRel.slice(0, fromRel.lastIndexOf('/')) : ''; const toRel = joinRel(parent, newName); if (toRel === fromRel) return { from: fromRel, to: toRel }; let dest = safeJoin(uploadDir, toRel); const resolved = resolveConflictPath(dest, fs.existsSync(dest) ? onConflict : 'rename'); if (!resolved) return null; dest = resolved; const finalRel = path.relative(uploadDir, dest).split(path.sep).join('/'); if (fs.existsSync(dest) && onConflict === 'overwrite') { await fsp.rm(dest, { recursive: true, force: true }); } await fsp.rename(src, dest); return { from: fromRel, to: finalRel }; }