feat: Ordner erstellen/löschen im File Explorer
Backend (WorkspaceEndpoint.cs): - POST /api/workspace/mkdir – Ordner im Session-Workspace erstellen - DELETE /api/workspace/dir – Leeren Ordner im Session-Workspace löschen - POST /api/workspace/agent/mkdir – Ordner im Agent-Memory erstellen - DELETE /api/workspace/agent/dir – Leeren Ordner im Agent-Memory löschen - POST /api/workspace/browse/mkdir – Ordner im Folder-Browser erstellen - Sicherheit: Path-Traversal-Schutz, Root nicht löschbar, nicht-leer → 409 Frontend (file-explorer.js): - 📁+ Button im Header für neuen Ordner (File Explorer + Folder Browser) - 🗑️ Button bei jedem Ordner (erscheint on hover, nur File Explorer) - Validierung: ungültige Zeichen werden abgelehnt CSS: - base.css: Icon-Button mit zwei Icons (folder + plus) - file-explorer.css: Ordner-Zeile als Flex (Label + Delete-Button)
This commit is contained in:
1 parent
d1355dcb0d
commit
8e7bda50f9
4 files changed
+335
-2
No files matched your search
@@ -41,10 +41,15 @@ public static class WorkspaceEndpoint
|
||||
app.MapGet("/api/workspace/file", HandleFile);
|
||||
app.MapPut("/api/workspace/file", HandleFileSave);
|
||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
||||
app.MapPost("/api/workspace/browse/mkdir", HandleBrowseMkdir);
|
||||
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);
|
||||
app.MapPost("/api/workspace/mkdir", HandleMkdir);
|
||||
app.MapDelete("/api/workspace/dir", HandleRmdir);
|
||||
app.MapPost("/api/workspace/agent/mkdir", HandleAgentMkdir);
|
||||
app.MapDelete("/api/workspace/agent/dir", HandleAgentRmdir);
|
||||
}
|
||||
|
||||
private static async Task HandleTree(HttpContext ctx)
|
||||
@@ -251,6 +256,38 @@ public static class WorkspaceEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new { current = targetPath.Replace('\\', '/'), entries });
|
||||
}
|
||||
|
||||
private static async Task HandleBrowseMkdir(HttpContext ctx)
|
||||
{
|
||||
var path = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(path);
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent muss existieren
|
||||
var parent = Path.GetDirectoryName(targetPath);
|
||||
if (string.IsNullOrEmpty(parent) || !Directory.Exists(parent))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Elternverzeichnis nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = targetPath.Replace('\\', '/'), created = true });
|
||||
}
|
||||
|
||||
private static bool IsLikelyTextFile(string name)
|
||||
{
|
||||
var lower = name.ToLowerInvariant();
|
||||
@@ -424,6 +461,174 @@ public static class WorkspaceEndpoint
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleMkdir(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
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, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
||||
}
|
||||
|
||||
private static async Task HandleRmdir(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
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, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
||||
}
|
||||
|
||||
private static async Task HandleAgentMkdir(HttpContext ctx)
|
||||
{
|
||||
var basePath = GetAgentBasePath(ctx, out var error);
|
||||
if (basePath == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
||||
return;
|
||||
}
|
||||
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
||||
}
|
||||
|
||||
private static async Task HandleAgentRmdir(HttpContext ctx)
|
||||
{
|
||||
var basePath = GetAgentBasePath(ctx, out var error);
|
||||
if (basePath == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
||||
return;
|
||||
}
|
||||
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> MimeTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" },
|
||||
|
||||
Reference in new issue
Block a user