using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Gott.Extensions; 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); app.MapPut("/api/workspace/file", HandleFileSave); app.MapGet("/api/workspace/browse", HandleBrowse); app.MapGet("/api/workspace/agent/tree", HandleAgentTree); app.MapGet("/api/workspace/agent/file", HandleAgentFile); app.MapPut("/api/workspace/agent/file", HandleAgentFileSave); app.MapGet("/api/workspace/raw", HandleRawFile); } 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 || session.UserId != ctx.GetUserId() || 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 || session.UserId != ctx.GetUserId() || 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 async Task HandleFileSave(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 || session.UserId != ctx.GetUserId() || 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; } using var reader = new StreamReader(ctx.Request.Body); var content = await reader.ReadToEndAsync(); await File.WriteAllTextAsync(targetPath, content); var info = new FileInfo(targetPath); var lineCount = content.Split('\n').Length; await ctx.Response.WriteAsJsonAsync(new { path = filePath, name = info.Name, size = info.Length, lineCount }); } private static async Task HandleBrowse(HttpContext ctx) { var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/"; var targetPath = Path.GetFullPath(path); if (!Directory.Exists(targetPath)) { ctx.Response.StatusCode = 404; await ctx.Response.WriteAsJsonAsync(new { error = "Verzeichnis nicht gefunden" }); return; } var entries = new List(); // Parent var parent = Directory.GetParent(targetPath); if (parent != null) { entries.Add(new { name = "..", path = parent.FullName.Replace('\\', '/'), type = "dir" }); } try { foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)) { var name = Path.GetFileName(dir); if (IgnoredDirs.Contains(name)) continue; entries.Add(new { name, path = dir.Replace('\\', '/'), type = "dir" }); } } catch (UnauthorizedAccessException) { } await ctx.Response.WriteAsJsonAsync(new { current = targetPath.Replace('\\', '/'), entries }); } 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"; } private static string GetAgentBasePath(HttpContext ctx, out string error) { error = null; var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); if (string.IsNullOrEmpty(agentId)) { error = "agentId fehlt"; return null; } var memoryService = ctx.RequestServices.GetRequiredService(); var basePath = memoryService.GetAgentMemoryDir(agentId); if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath); return basePath; } private static async Task HandleAgentTree(HttpContext ctx) { var basePath = GetAgentBasePath(ctx, out var error); if (basePath == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error }); return; } var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath)); 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(); 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" }); } 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 HandleAgentFile(HttpContext ctx) { var basePath = GetAgentBasePath(ctx, out var error); if (basePath == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error }); return; } var filePath = ctx.Request.Query["path"].FirstOrDefault(); if (string.IsNullOrEmpty(filePath)) { ctx.Response.StatusCode = 400; return; } 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); 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 async Task HandleAgentFileSave(HttpContext ctx) { var basePath = GetAgentBasePath(ctx, out var error); if (basePath == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error }); return; } var filePath = ctx.Request.Query["path"].FirstOrDefault(); if (string.IsNullOrEmpty(filePath)) { ctx.Response.StatusCode = 400; return; } var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); if (!targetPath.StartsWith(basePath)) { ctx.Response.StatusCode = 403; return; } // Verzeichnis erstellen falls nötig var dir = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); using var reader = new StreamReader(ctx.Request.Body); var content = await reader.ReadToEndAsync(); await File.WriteAllTextAsync(targetPath, content); var info = new FileInfo(targetPath); var lineCount = content.Split('\n').Length; await ctx.Response.WriteAsJsonAsync(new { path = filePath, name = info.Name, size = info.Length, lineCount }); } private static readonly Dictionary MimeTypes = new(StringComparer.OrdinalIgnoreCase) { { ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".svg", "image/svg+xml" }, { ".bmp", "image/bmp" }, { ".ico", "image/x-icon" }, { ".pdf", "application/pdf" }, { ".mp4", "video/mp4" }, { ".webm", "video/webm" }, { ".mp3", "audio/mpeg" }, { ".wav", "audio/wav" }, { ".ogg", "audio/ogg" }, }; private static async Task HandleRawFile(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 || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) { ctx.Response.StatusCode = 400; 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 ext = Path.GetExtension(targetPath); var contentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream"); ctx.Response.ContentType = contentType; ctx.Response.Headers["Cache-Control"] = "private, max-age=3600"; await ctx.Response.SendFileAsync(targetPath); } }