Explorar o código

feat: add server file ops

Thomas Zhang hai 2 meses
pai
achega
4b66bf0fd9
Modificáronse 1 ficheiros con 88 adicións e 0 borrados
  1. 88 0
      src/lib/server/server.ts

+ 88 - 0
src/lib/server/server.ts

@@ -1,3 +1,7 @@
+import type { Stats } from 'node:fs';
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
 import { serverLogger as log } from '$lib/server/logger';
 import type { Workspace } from '$lib/types/workspace';
 
@@ -11,6 +15,7 @@ import { WorkspaceManager } from './workspace';
 class Server {
 	// Guards against double-init in HMR / test environments.
 	#initialized = false;
+	#fileSavePath = env.FILE_SAVE_PATH;
 
 	#ws!: WorkspaceManager;
 	#auth: Auth | undefined;
@@ -78,6 +83,89 @@ class Server {
 	get resources(): Resources {
 		return this.#resources;
 	}
+
+	async #saveFile(fileName: string, file: File): Promise<string> {
+		await fs.mkdir(path.dirname(fileName), { recursive: true });
+		let usedFileName = fileName;
+		while (true) {
+			const stat = await fs.stat(usedFileName).catch(() => undefined);
+			if (stat) {
+				// rename the file
+				const ext = path.extname(fileName);
+				const base = path.basename(fileName, ext);
+				const dir = path.dirname(fileName);
+				usedFileName = path.join(dir, `${base}-${Date.now()}${ext}`);
+			} else {
+				break;
+			}
+		}
+		const buffer = Buffer.from(await file.arrayBuffer());
+		await fs.writeFile(usedFileName, buffer);
+		return usedFileName;
+	}
+
+	async saveFiles(
+		workspaceId: string,
+		files: Record<string, File>,
+	): Promise<Record<string, string>> {
+		const root = this.#fileSavePath;
+		if (root === undefined) {
+			throw new Error('FILE_SAVE_PATH env var is not set');
+		}
+		const saved: Record<string, string> = {};
+		const savePromises = Object.entries(files).map(([fileName, file]) =>
+			(async () => {
+				const savedPath = await this.#saveFile(path.join(root, workspaceId, fileName), file);
+				saved[fileName] = path.relative(root, savedPath);
+			})(),
+		);
+		await Promise.all(savePromises);
+		return saved;
+	}
+
+	#getFilePath(workspaceId: string, filePath: string): string {
+		const root = this.#fileSavePath;
+		if (root === undefined) {
+			throw new Error('FILE_SAVE_PATH env var is not set');
+		}
+		const normalizedPath = path.normalize(filePath);
+		if (path.isAbsolute(normalizedPath) || normalizedPath.startsWith('..')) {
+			throw new Error('Invalid file path');
+		}
+
+		const absolutePath = path.resolve(root, normalizedPath);
+		const allowedRoot = path.resolve(root, workspaceId);
+		if (!absolutePath.startsWith(`${allowedRoot}${path.sep}`) && absolutePath !== allowedRoot) {
+			throw new Error('Invalid file path');
+		}
+		return absolutePath;
+	}
+
+	async readFile(
+		workspaceId: string,
+		filePath: string,
+	): Promise<{ stats: Stats; content: Buffer<ArrayBuffer> }> {
+		const absolutePath = this.#getFilePath(workspaceId, filePath);
+		let stats: Stats;
+		try {
+			stats = await fs.stat(absolutePath);
+		} catch {
+			throw new Error('File not found');
+		}
+		if (!stats.isFile()) throw new Error('File not found');
+
+		return { stats, content: await fs.readFile(absolutePath) };
+	}
+
+	async deleteFile(workspaceId: string, filePath: string): Promise<void> {
+		const absolutePath = this.#getFilePath(workspaceId, filePath);
+		try {
+			await fs.unlink(absolutePath);
+		} catch (err: any) {
+			if (err.code === 'ENOENT') return;
+			throw err;
+		}
+	}
 }
 
 // Singleton shared across hooks and route handlers.