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

82 lines
3.0 KiB
JavaScript

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