using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace Gott.Endpoints; public static class WorkspaceEndpoint { private static readonly HashSet TextExtensions = new(StringComparer.OrdinalIgnoreCase) { ".cs", ".csproj", ".sln", ".json", ".xml", ".yaml", ".yml", ".toml", ".md", ".txt", ".log", ".csv", ".tsv", ".html", ".htm", ".css", ".js", ".ts", ".jsx", ".tsx", ".sh", ".bash", ".bat", ".ps1", ".cmd", ".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h", ".hpp", ".sql", ".graphql", ".env", ".gitignore", ".dockerignore", ".editorconfig", ".razor", ".cshtml", ".vue", ".svelte", ".ini", ".cfg", ".conf", ".properties", ".dockerfile", ".makefile", "" // Dateien ohne Extension (Makefile, Dockerfile, etc.) }; private static readonly HashSet IgnoredDirs = new(StringComparer.OrdinalIgnoreCase) { "bin", "obj", "node_modules", ".git", ".vs", ".idea", "publish", "dist", "out", "build", "target", ".cache" }; public static void MapWorkspaceEndpoint(this IEndpointRouteBuilder app) { app.MapGet("/api/workspace/tree", HandleTree); app.MapGet("/api/workspace/file", HandleFile); } private static async Task HandleTree(HttpContext ctx) { var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault(); var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; // Workspace aus Session holen var db = ctx.RequestServices.GetRequiredService(); var session = await db.GetSession(sessionId ?? ""); if (session == null || string.IsNullOrEmpty(session.Workspace)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); return; } var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath)); // Sicherheit: nicht aus dem Workspace ausbrechen if (!targetPath.StartsWith(basePath)) { ctx.Response.StatusCode = 403; return; } if (!Directory.Exists(targetPath)) { ctx.Response.StatusCode = 404; await ctx.Response.WriteAsJsonAsync(new { error = "Verzeichnis nicht gefunden" }); return; } var entries = new List(); // Ordner foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)) { var name = Path.GetFileName(dir); if (IgnoredDirs.Contains(name)) continue; var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/'); entries.Add(new { name, path = relPath, type = "dir" }); } // Dateien foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)) { var name = Path.GetFileName(file); var ext = Path.GetExtension(file); var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/'); var size = new FileInfo(file).Length; var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name); entries.Add(new { name, path = relPath, type = "file", ext, size, isText }); } await ctx.Response.WriteAsJsonAsync(entries); } private static async Task HandleFile(HttpContext ctx) { var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault(); var filePath = ctx.Request.Query["path"].FirstOrDefault(); if (string.IsNullOrEmpty(filePath)) { ctx.Response.StatusCode = 400; return; } var db = ctx.RequestServices.GetRequiredService(); var session = await db.GetSession(sessionId ?? ""); if (session == null || string.IsNullOrEmpty(session.Workspace)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); return; } var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); if (!targetPath.StartsWith(basePath)) { ctx.Response.StatusCode = 403; return; } if (!File.Exists(targetPath)) { ctx.Response.StatusCode = 404; return; } var info = new FileInfo(targetPath); // Max 2MB für Textdateien if (info.Length > 2 * 1024 * 1024) { ctx.Response.StatusCode = 413; await ctx.Response.WriteAsJsonAsync(new { error = "Datei zu groß (max. 2MB)" }); return; } var content = await File.ReadAllTextAsync(targetPath); var lineCount = content.Split('\n').Length; await ctx.Response.WriteAsJsonAsync(new { path = filePath, name = info.Name, size = info.Length, lineCount, content }); } private static bool IsLikelyTextFile(string name) { var lower = name.ToLowerInvariant(); return lower is "makefile" or "dockerfile" or "rakefile" or "gemfile" or "procfile" or "vagrantfile" or "license" or "readme" or "changelog" or "authors" or "contributors"; } }