File Explorer Modal: Workspace-Browser mit Textvorschau, Zeilennummern, Selektion als File-Picker im Chat
This commit is contained in:
1 parent
663f2c2a9a
commit
491d3b916e
6 files changed
+682
No files matched your search
@@ -0,0 +1,167 @@
|
||||
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<string> 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<string> 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<Gott.Services.DatabaseService>();
|
||||
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<object>();
|
||||
|
||||
// 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<Gott.Services.DatabaseService>();
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user