Skip to main content
Glama
file-utils.ts5.89 kB
import { readdir, unlink, stat, mkdir, readFile, writeFile, rmdir } from "fs/promises"; import { join, resolve } from "path"; import { tmpdir } from "os"; import { APP_NAME, PREVIEW_ID_REGEX, UNIX_SYSTEM_PATHS, WINDOWS_SYSTEM_PATHS, TIMEOUTS, FILE_NAMES, DIR_NAMES, } from "./constants.js"; import type { DiagramOptions } from "./types.js"; import { webLogger } from "./logger.js"; export function getConfigDir(): string { const xdg = process.env.XDG_CONFIG_HOME; if (xdg && xdg.trim()) return xdg; const homeDir = process.env.HOME || process.env.USERPROFILE || tmpdir(); return join(homeDir, ".config"); } export function getAppDir(): string { return join(getConfigDir(), APP_NAME); } export function getLiveDir(): string { return join(getAppDir(), DIR_NAMES.LIVE); } export function getLogsDir(): string { return join(getAppDir(), DIR_NAMES.LOGS); } /** * Validates that previewId is safe to use in file paths * Only allows alphanumeric characters, hyphens, and underscores */ export function validatePreviewId(previewId: string): void { if (!previewId || !PREVIEW_ID_REGEX.test(previewId)) { throw new Error( "Invalid preview ID format. Only alphanumeric characters, hyphens, and underscores are allowed." ); } } export function getPreviewDir(previewId: string): string { validatePreviewId(previewId); return join(getLiveDir(), previewId); } export function getDiagramFilePath(previewId: string, format: string): string { return join(getPreviewDir(previewId), `diagram.${format}`); } export function getDiagramSourcePath(previewId: string): string { return join(getPreviewDir(previewId), FILE_NAMES.DIAGRAM_SOURCE); } export function getDiagramOptionsPath(previewId: string): string { return join(getPreviewDir(previewId), FILE_NAMES.DIAGRAM_OPTIONS); } // Re-export DiagramOptions type from types.ts for backward compatibility export type { DiagramOptions } from "./types.js"; // Re-export DEFAULT_DIAGRAM_OPTIONS from constants.ts for backward compatibility export { DEFAULT_DIAGRAM_OPTIONS } from "./constants.js"; export async function saveDiagramSource( previewId: string, diagram: string, options: DiagramOptions ): Promise<void> { const sourcePath = getDiagramSourcePath(previewId); const optionsPath = getDiagramOptionsPath(previewId); await writeFile(sourcePath, diagram, "utf-8"); await writeFile(optionsPath, JSON.stringify(options, null, 2), "utf-8"); } export async function loadDiagramSource(previewId: string): Promise<string> { const sourcePath = getDiagramSourcePath(previewId); return await readFile(sourcePath, "utf-8"); } export async function loadDiagramOptions(previewId: string): Promise<DiagramOptions> { const optionsPath = getDiagramOptionsPath(previewId); const content = await readFile(optionsPath, "utf-8"); return JSON.parse(content); } /** * Helper function to delete a directory and all its contents * @param dirPath - The absolute path to the directory to delete */ async function deleteDiagramDirectory(dirPath: string): Promise<void> { const files = await readdir(dirPath); for (const file of files) { await unlink(join(dirPath, file)); } await rmdir(dirPath); } export async function deleteDiagram(previewId: string): Promise<void> { validatePreviewId(previewId); const dirPath = getPreviewDir(previewId); try { await deleteDiagramDirectory(dirPath); } catch (error) { throw new Error( `Failed to delete diagram: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Validates a save path to prevent path traversal and writing to sensitive locations * @param savePath - The path where the user wants to save the file * @throws Error if the path is invalid or dangerous */ export function validateSavePath(savePath: string): void { // Check for null bytes (potential security issue) if (savePath.includes("\0")) { throw new Error("Path contains null bytes"); } // Resolve to absolute path const absolutePath = resolve(savePath); // Prevent writing to sensitive system directories (Unix/Linux/macOS) if ( process.platform !== "win32" && UNIX_SYSTEM_PATHS.some((danger) => absolutePath.startsWith(danger)) ) { throw new Error("Cannot write to system directories"); } // Prevent writing to Windows system directories if (process.platform === "win32") { const normalizedPath = absolutePath.replace(/\//g, "\\"); if (WINDOWS_SYSTEM_PATHS.some((danger) => normalizedPath.startsWith(danger))) { throw new Error("Cannot write to system directories"); } } } export async function cleanupOldDiagrams( maxAgeMs: number = TIMEOUTS.CLEANUP_MAX_AGE_MS ): Promise<number> { try { const liveDir = getLiveDir(); await mkdir(liveDir, { recursive: true }); const entries = await readdir(liveDir, { withFileTypes: true }); const now = Date.now(); let cleanedCount = 0; for (const entry of entries) { if (entry.isDirectory()) { const dirPath = join(liveDir, entry.name); const sourcePath = join(dirPath, FILE_NAMES.DIAGRAM_SOURCE); try { const stats = await stat(sourcePath); const age = now - stats.mtimeMs; if (age > maxAgeMs) { await deleteDiagramDirectory(dirPath); webLogger.info(`Cleaned up old diagram: ${entry.name}`); cleanedCount++; } } catch (error) { // Log but continue if individual diagram cleanup fails webLogger.debug(`Skipping diagram cleanup: ${entry.name}`, { error: error instanceof Error ? error.message : String(error), }); } } } return cleanedCount; } catch (error) { webLogger.warn("Diagram cleanup failed", { error: error instanceof Error ? error.message : String(error), }); return 0; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/veelenga/claude-mermaid'

If you have feedback or need assistance with the MCP directory API, please join our Discord server