feat: Windows-Kompatibilität
- WorkspaceHelper: Zentrale Hilfsmethoden (IsWindows, TempDir, IsSubPathOf, GetRelativePath, ShellExecutable, ShellArguments) - ToolService: execute nutzt cmd.exe auf Windows, GetRelativePath mit Forward-Slashes, IsSubPathOf für case-insensitive Pfadprüfung - SessionEndpoint: /tmp → Path.GetTempPath(), bash → cmd.exe, Fire-and-forget via .bat statt systemd-run auf Windows - ChatService: /tmp → WorkspaceHelper.TempDir - MemoryService: IsSubPathOf + GetRelativePath - WorkspaceEndpoint: HandleBrowse zeigt Laufwerke auf Windows, IsSubPathOf statt StartsWith - InfoEndpoint: gott.exe auf Windows, isWindows-Flag in API-Response - Frontend: File-Explorer mit Laufwerks-Navigation auf Windows
This commit is contained in:
1 parent
79899a6981
commit
5d090e160b
9 files changed
+155
-61
No files matched your search
@@ -29,7 +29,8 @@ public static class InfoEndpoint
|
||||
else
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
var exePath = Path.Combine(dir, "gott");
|
||||
var exeName = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) ? "gott.exe" : "gott";
|
||||
var exePath = Path.Combine(dir, exeName);
|
||||
if (File.Exists(exePath))
|
||||
{
|
||||
var lastWrite = File.GetLastWriteTimeUtc(exePath);
|
||||
@@ -37,6 +38,7 @@ public static class InfoEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Json(new { buildTime });
|
||||
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
|
||||
return Results.Json(new { buildTime, isWindows });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -234,7 +235,7 @@ public static class SessionEndpoint
|
||||
var user = await db.GetUser(userId);
|
||||
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(workspace) && skills.Any())
|
||||
workspace = "/tmp";
|
||||
workspace = WorkspaceHelper.TempDir;
|
||||
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
||||
|
||||
return (session, agent, marked, memDir, skills, user, workspace, planMode);
|
||||
@@ -317,7 +318,7 @@ public static class SessionEndpoint
|
||||
|
||||
var user = await db.GetUser(userId);
|
||||
var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : "/tmp";
|
||||
var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : WorkspaceHelper.TempDir;
|
||||
|
||||
if (mode == "foreground")
|
||||
{
|
||||
@@ -326,14 +327,16 @@ public static class SessionEndpoint
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
FileName = WorkspaceHelper.ShellExecutable,
|
||||
WorkingDirectory = workingDir,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardInput = !WorkspaceHelper.IsWindows,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
if (WorkspaceHelper.IsWindows)
|
||||
psi.Arguments = WorkspaceHelper.ShellArguments(command);
|
||||
|
||||
using var proc = System.Diagnostics.Process.Start(psi);
|
||||
if (proc == null)
|
||||
@@ -343,8 +346,11 @@ public static class SessionEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
await proc.StandardInput.WriteAsync(command);
|
||||
proc.StandardInput.Close();
|
||||
if (!WorkspaceHelper.IsWindows)
|
||||
{
|
||||
await proc.StandardInput.WriteAsync(command);
|
||||
proc.StandardInput.Close();
|
||||
}
|
||||
|
||||
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
try
|
||||
@@ -379,22 +385,40 @@ public static class SessionEndpoint
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fire-and-forget via systemd-run
|
||||
// Fire-and-forget
|
||||
try
|
||||
{
|
||||
var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh";
|
||||
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
ProcessStartInfo psi;
|
||||
if (WorkspaceHelper.IsWindows)
|
||||
{
|
||||
FileName = "systemd-run",
|
||||
Arguments = $"--scope --quiet bash {scriptPath}",
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.bat");
|
||||
await System.IO.File.WriteAllTextAsync(scriptPath, $"@echo off\r\ncd /d \"{workingDir}\"\r\n{command}\r\ndel \"%~f0\"\r\n");
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c \"{scriptPath}\"",
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.sh");
|
||||
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "systemd-run",
|
||||
Arguments = $"--scope --quiet bash {scriptPath}",
|
||||
WorkingDirectory = workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
|
||||
var process = Process.Start(psi);
|
||||
if (process != null)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Gott.Services;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
@@ -83,7 +84,7 @@ public static class WorkspaceEndpoint
|
||||
|
||||
var userId = ctx.GetUserId();
|
||||
var user = await db.GetUser(userId);
|
||||
var resolved = Gott.Services.WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
var resolved = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolved))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
@@ -125,7 +126,7 @@ public static class WorkspaceEndpoint
|
||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
@@ -144,7 +145,7 @@ public static class WorkspaceEndpoint
|
||||
{
|
||||
var name = Path.GetFileName(dir);
|
||||
if (IgnoredDirs.Contains(name)) continue;
|
||||
var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/');
|
||||
var relPath = WorkspaceHelper.GetRelativePath(basePath, dir);
|
||||
entries.Add(new { name, path = relPath, type = "dir" });
|
||||
}
|
||||
|
||||
@@ -152,7 +153,7 @@ public static class WorkspaceEndpoint
|
||||
{
|
||||
var name = Path.GetFileName(file);
|
||||
var ext = Path.GetExtension(file);
|
||||
var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/');
|
||||
var relPath = WorkspaceHelper.GetRelativePath(basePath, file);
|
||||
var size = new FileInfo(file).Length;
|
||||
var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name);
|
||||
entries.Add(new { name, path = relPath, type = "file", ext, size, isText });
|
||||
@@ -173,7 +174,7 @@ public static class WorkspaceEndpoint
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
@@ -218,7 +219,7 @@ public static class WorkspaceEndpoint
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
@@ -271,7 +272,7 @@ public static class WorkspaceEndpoint
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
@@ -287,7 +288,7 @@ public static class WorkspaceEndpoint
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
path = Path.GetRelativePath(basePath, targetPath).Replace('\\', '/'),
|
||||
path = WorkspaceHelper.GetRelativePath(basePath, targetPath),
|
||||
created = true
|
||||
});
|
||||
}
|
||||
@@ -296,7 +297,18 @@ public static class WorkspaceEndpoint
|
||||
|
||||
private static async Task HandleBrowse(HttpContext ctx)
|
||||
{
|
||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? (WorkspaceHelper.IsWindows ? "" : "/");
|
||||
|
||||
// Windows: Leerer Pfad oder Root → Laufwerke anzeigen
|
||||
if (WorkspaceHelper.IsWindows && string.IsNullOrEmpty(path))
|
||||
{
|
||||
var drives = System.IO.DriveInfo.GetDrives()
|
||||
.Where(d => d.IsReady)
|
||||
.Select(d => new { name = d.Name.TrimEnd('\\'), path = d.Name.Replace('\\', '/'), type = "dir" })
|
||||
.ToList<object>();
|
||||
await ctx.Response.WriteAsJsonAsync(new { current = "", entries = drives });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(path);
|
||||
if (!Directory.Exists(targetPath))
|
||||
@@ -368,7 +380,7 @@ public static class WorkspaceEndpoint
|
||||
if (basePath == null) return;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ChatService
|
||||
|
||||
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
|
||||
workspace = "/tmp";
|
||||
workspace = WorkspaceHelper.TempDir;
|
||||
|
||||
var promptBlocks = SystemPromptBuilder.BuildBlocks(
|
||||
agent, session, markedMessages, _memoryService, agentMemoryDir,
|
||||
|
||||
@@ -81,7 +81,7 @@ public class MemoryService
|
||||
? baseDir
|
||||
: Path.GetFullPath(Path.Combine(baseDir, subPath));
|
||||
|
||||
if (!targetDir.StartsWith(baseDir))
|
||||
if (!WorkspaceHelper.IsSubPathOf(targetDir, baseDir))
|
||||
throw new Exception("Zugriff außerhalb des Memory-Verzeichnisses nicht erlaubt");
|
||||
|
||||
if (!Directory.Exists(targetDir))
|
||||
@@ -89,9 +89,9 @@ public class MemoryService
|
||||
|
||||
var entries = new List<string>();
|
||||
foreach (var dir in Directory.GetDirectories(targetDir))
|
||||
entries.Add(Path.GetRelativePath(baseDir, dir) + "/");
|
||||
entries.Add(WorkspaceHelper.GetRelativePath(baseDir, dir) + "/");
|
||||
foreach (var file in Directory.GetFiles(targetDir))
|
||||
entries.Add(Path.GetRelativePath(baseDir, file));
|
||||
entries.Add(WorkspaceHelper.GetRelativePath(baseDir, file));
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ public class MemoryService
|
||||
{
|
||||
var baseDir = GetAgentMemoryDir(agentId);
|
||||
var full = Path.GetFullPath(Path.Combine(baseDir, fileName));
|
||||
if (!full.StartsWith(baseDir))
|
||||
if (!WorkspaceHelper.IsSubPathOf(full, baseDir))
|
||||
return null;
|
||||
return full;
|
||||
}
|
||||
|
||||
+22
-17
@@ -300,7 +300,7 @@ public class ToolService
|
||||
return "Fehler: Kein Workspace konfiguriert";
|
||||
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
|
||||
if (!File.Exists(path))
|
||||
return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
case "write_file":
|
||||
@@ -312,7 +312,7 @@ public class ToolService
|
||||
if (dir != null) Directory.CreateDirectory(dir);
|
||||
var content = input.GetProperty("content").GetString() ?? "";
|
||||
File.WriteAllText(path, content);
|
||||
return $"Datei geschrieben: {Path.GetRelativePath(workspace, path)} ({content.Length} Zeichen)";
|
||||
return $"Datei geschrieben: {WorkspaceHelper.GetRelativePath(workspace, path)} ({content.Length} Zeichen)";
|
||||
}
|
||||
case "edit_file":
|
||||
{
|
||||
@@ -320,7 +320,7 @@ public class ToolService
|
||||
return "Fehler: Kein Workspace konfiguriert";
|
||||
var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
|
||||
if (!File.Exists(path))
|
||||
return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
var fileContent = File.ReadAllText(path);
|
||||
var oldText = input.GetProperty("old_text").GetString() ?? "";
|
||||
var newText = input.GetProperty("new_text").GetString() ?? "";
|
||||
@@ -328,7 +328,7 @@ public class ToolService
|
||||
return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig.";
|
||||
fileContent = fileContent.Replace(oldText, newText);
|
||||
File.WriteAllText(path, fileContent);
|
||||
return $"Datei bearbeitet: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Datei bearbeitet: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
}
|
||||
case "list_files":
|
||||
{
|
||||
@@ -336,12 +336,12 @@ public class ToolService
|
||||
return "Fehler: Kein Workspace konfiguriert";
|
||||
var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace);
|
||||
if (!Directory.Exists(path))
|
||||
return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Fehler: Verzeichnis nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
var entries = new List<string>();
|
||||
foreach (var dir in Directory.GetDirectories(path))
|
||||
entries.Add(Path.GetRelativePath(workspace, dir) + "/");
|
||||
entries.Add(WorkspaceHelper.GetRelativePath(workspace, dir) + "/");
|
||||
foreach (var file in Directory.GetFiles(path))
|
||||
entries.Add(Path.GetRelativePath(workspace, file));
|
||||
entries.Add(WorkspaceHelper.GetRelativePath(workspace, file));
|
||||
return entries.Count == 0 ? "(leer)" : string.Join("\n", entries);
|
||||
}
|
||||
case "delete_file":
|
||||
@@ -352,14 +352,14 @@ public class ToolService
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
return $"Datei gelöscht: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Datei gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
}
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, recursive: false);
|
||||
return $"Verzeichnis gelöscht: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
}
|
||||
return $"Fehler: Nicht gefunden: {Path.GetRelativePath(workspace, path)}";
|
||||
return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||||
}
|
||||
case "execute":
|
||||
{
|
||||
@@ -368,18 +368,23 @@ public class ToolService
|
||||
var command = input.GetProperty("command").GetString() ?? "";
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
FileName = WorkspaceHelper.ShellExecutable,
|
||||
WorkingDirectory = workspace,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardInput = !WorkspaceHelper.IsWindows,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
if (WorkspaceHelper.IsWindows)
|
||||
psi.Arguments = WorkspaceHelper.ShellArguments(command);
|
||||
using var proc = Process.Start(psi);
|
||||
if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden";
|
||||
await proc.StandardInput.WriteAsync(command);
|
||||
proc.StandardInput.Close();
|
||||
if (!WorkspaceHelper.IsWindows)
|
||||
{
|
||||
await proc.StandardInput.WriteAsync(command);
|
||||
proc.StandardInput.Close();
|
||||
}
|
||||
// Timeout (180s) UND externen CancellationToken verknüpfen
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(180));
|
||||
@@ -423,7 +428,7 @@ public class ToolService
|
||||
var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot));
|
||||
var result = matcher.Execute(dirInfo);
|
||||
var matches = result.Files
|
||||
.Select(f => Path.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path)))
|
||||
.Select(f => WorkspaceHelper.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path)))
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches);
|
||||
@@ -459,7 +464,7 @@ public class ToolService
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
if (regex.IsMatch(lines[i]))
|
||||
results.Add($"{Path.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}");
|
||||
results.Add($"{WorkspaceHelper.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}");
|
||||
}
|
||||
}
|
||||
if (results.Count == 0) return "(keine Treffer)";
|
||||
@@ -521,7 +526,7 @@ public class ToolService
|
||||
private string ResolvePath(string relativePath, string workspace)
|
||||
{
|
||||
var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? ""));
|
||||
if (!full.StartsWith(workspace))
|
||||
if (!WorkspaceHelper.IsSubPathOf(full, workspace))
|
||||
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
|
||||
return full;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Gott.Models;
|
||||
|
||||
namespace Gott.Services;
|
||||
|
||||
public static class WorkspaceHelper
|
||||
{
|
||||
/// <summary>True wenn die Plattform Windows ist.</summary>
|
||||
internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
/// <summary>Plattformunabhängiges Temp-Verzeichnis ohne Trailing-Separator.</summary>
|
||||
internal static string TempDir => Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
/// <summary>
|
||||
/// Prüft ob ein Pfad ein Sub-Pfad eines Basis-Pfads ist (oder gleich).
|
||||
/// Auf Windows case-insensitive, auf Linux case-sensitive.
|
||||
/// </summary>
|
||||
internal static bool IsSubPathOf(string path, string basePath)
|
||||
{
|
||||
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
var normalizedPath = Path.GetFullPath(path);
|
||||
var normalizedBase = Path.GetFullPath(basePath);
|
||||
return normalizedPath.Equals(normalizedBase, comparison)
|
||||
|| normalizedPath.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt den relativen Pfad zurück – immer mit Forward-Slashes (für Agent-Output/API).
|
||||
/// </summary>
|
||||
internal static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
var rel = Path.GetRelativePath(relativeTo, path);
|
||||
return IsWindows ? rel.Replace('\\', '/') : rel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt den Shell-Dateinamen zurück (cmd.exe auf Windows, /bin/bash auf Linux).
|
||||
/// </summary>
|
||||
internal static string ShellExecutable => IsWindows ? "cmd.exe" : "/bin/bash";
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die Shell-Argumente zurück um einen Befehl auszuführen.
|
||||
/// Windows: /c "command", Linux: via stdin.
|
||||
/// </summary>
|
||||
internal static string ShellArguments(string command)
|
||||
=> IsWindows ? $"/c \"{command.Replace("\"", "\\\"")}\"" : "";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Löst den Workspace-Pfad einer Session auf.
|
||||
/// session.Workspace ist relativ zum user.DefaultPath.
|
||||
@@ -30,8 +72,7 @@ public static class WorkspaceHelper
|
||||
var resolved = Path.GetFullPath(Path.Combine(basePath, workspace));
|
||||
|
||||
// Sicherheit: darf nicht aus dem DefaultPath ausbrechen
|
||||
var normalizedBase = Path.GetFullPath(basePath);
|
||||
if (!resolved.StartsWith(normalizedBase + Path.DirectorySeparatorChar) && resolved != normalizedBase)
|
||||
if (!IsSubPathOf(resolved, Path.GetFullPath(basePath)))
|
||||
return null;
|
||||
|
||||
return resolved;
|
||||
@@ -47,7 +88,7 @@ public static class WorkspaceHelper
|
||||
if (string.IsNullOrWhiteSpace(workspace))
|
||||
return null;
|
||||
|
||||
var trimmed = workspace.Trim().TrimStart('/').TrimEnd('/');
|
||||
var trimmed = workspace.Trim().TrimStart('/', '\\').TrimEnd('/', '\\');
|
||||
if (string.IsNullOrEmpty(trimmed))
|
||||
return null;
|
||||
|
||||
@@ -76,11 +117,12 @@ public static class WorkspaceHelper
|
||||
|
||||
var normalizedBase = Path.GetFullPath(defaultPath.Trim());
|
||||
var normalizedWorkspace = Path.GetFullPath(absoluteWorkspace.Trim());
|
||||
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
|
||||
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar))
|
||||
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison))
|
||||
return normalizedWorkspace.Substring(normalizedBase.Length + 1);
|
||||
|
||||
if (normalizedWorkspace == normalizedBase)
|
||||
if (normalizedWorkspace.Equals(normalizedBase, comparison))
|
||||
return "";
|
||||
|
||||
return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy)
|
||||
|
||||
@@ -535,7 +535,7 @@ function openFolderBrowser(opts = {}) {
|
||||
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
|
||||
document.addEventListener('keydown', _feState._keyHandler);
|
||||
|
||||
fbLoadDir(opts.startPath || '/');
|
||||
fbLoadDir(opts.startPath || (serverIsWindows ? '' : '/'));
|
||||
}
|
||||
|
||||
async function fbLoadDir(absPath) {
|
||||
@@ -581,7 +581,7 @@ async function fbLoadDir(absPath) {
|
||||
|
||||
const currentNorm = data.current.replace(/\/+$/, '');
|
||||
const atBase = _feState.basePath && currentNorm === _feState.basePath;
|
||||
const atRoot = currentNorm === '' || currentNorm === '/';
|
||||
const atRoot = currentNorm === '' || currentNorm === '/' || (serverIsWindows && /^[A-Z]:$/i.test(currentNorm));
|
||||
document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0);
|
||||
} catch (e) {
|
||||
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
|
||||
@@ -605,7 +605,14 @@ function fbGoBack() {
|
||||
fbLoadDir(prev);
|
||||
} else {
|
||||
// Kein History-Eintrag → zum Parent navigieren
|
||||
const parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || '/' : '/';
|
||||
let parent;
|
||||
if (serverIsWindows && /^[A-Z]:$/i.test(currentNorm)) {
|
||||
parent = ''; // Laufwerk → zurück zur Laufwerksauswahl
|
||||
} else if (serverIsWindows && /^[A-Z]:\/[^/]*$/i.test(currentNorm)) {
|
||||
parent = currentNorm.substring(0, 2); // z.B. C:/Users → C:
|
||||
} else {
|
||||
parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || (serverIsWindows ? '' : '/') : (serverIsWindows ? '' : '/');
|
||||
}
|
||||
if (parent !== currentNorm) {
|
||||
fbLoadDir(parent);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
let buildTime = 0;
|
||||
let serverIsWindows = false;
|
||||
|
||||
fetch('/api/info').then(r => r.json()).then(data => {
|
||||
buildTime = data.buildTime || 0;
|
||||
serverIsWindows = !!data.isWindows;
|
||||
updateTitle();
|
||||
setInterval(updateTitle, 1000);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user