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)
684 lines
23 KiB
C#
684 lines
23 KiB
C#
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<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);
|
|
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)
|
|
{
|
|
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 || 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<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 || 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<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, 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<object>();
|
|
|
|
// 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 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();
|
|
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<Gott.Services.MemoryService>();
|
|
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<object>();
|
|
|
|
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 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" },
|
|
{ ".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<Gott.Services.DatabaseService>();
|
|
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);
|
|
}
|
|
}
|