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>
82 lines
3.0 KiB
JavaScript
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 };
|
|
}
|