- 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
131 lines
5.0 KiB
C#
131 lines
5.0 KiB
C#
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.
|
||
/// Gibt den absoluten Pfad zurück oder null wenn kein Workspace konfiguriert ist.
|
||
/// </summary>
|
||
public static string ResolveWorkspace(SessionRecord session, UserRecord user)
|
||
{
|
||
if (session == null || string.IsNullOrWhiteSpace(session.Workspace))
|
||
return null;
|
||
|
||
var workspace = session.Workspace.Trim();
|
||
|
||
// Wenn der Workspace bereits ein absoluter Pfad ist (Legacy/Fallback)
|
||
if (Path.IsPathRooted(workspace))
|
||
return Path.GetFullPath(workspace);
|
||
|
||
// Relativer Pfad → mit DefaultPath des Users kombinieren
|
||
var basePath = user?.DefaultPath?.Trim();
|
||
if (string.IsNullOrWhiteSpace(basePath))
|
||
return null; // Kein DefaultPath gesetzt → relativer Workspace kann nicht aufgelöst werden
|
||
|
||
var resolved = Path.GetFullPath(Path.Combine(basePath, workspace));
|
||
|
||
// Sicherheit: darf nicht aus dem DefaultPath ausbrechen
|
||
if (!IsSubPathOf(resolved, Path.GetFullPath(basePath)))
|
||
return null;
|
||
|
||
return resolved;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Validiert einen relativen Workspace-Pfad.
|
||
/// Prüft auf Path-Traversal (.. etc.).
|
||
/// Gibt den bereinigten Pfad zurück oder null bei ungültigem Pfad.
|
||
/// </summary>
|
||
public static string ValidateRelativeWorkspace(string workspace)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(workspace))
|
||
return null;
|
||
|
||
var trimmed = workspace.Trim().TrimStart('/', '\\').TrimEnd('/', '\\');
|
||
if (string.IsNullOrEmpty(trimmed))
|
||
return null;
|
||
|
||
// Path-Traversal verhindern
|
||
if (trimmed.Contains("..") || trimmed.Contains("~"))
|
||
return null;
|
||
|
||
// Keine absoluten Pfade erlauben
|
||
if (Path.IsPathRooted(trimmed))
|
||
return null;
|
||
|
||
return trimmed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Konvertiert einen absoluten Workspace-Pfad in einen relativen Pfad zum DefaultPath.
|
||
/// Wird beim Speichern verwendet, damit alte absolute Pfade in relative umgewandelt werden.
|
||
/// </summary>
|
||
public static string ToRelativeWorkspace(string absoluteWorkspace, string defaultPath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(absoluteWorkspace))
|
||
return null;
|
||
|
||
if (string.IsNullOrWhiteSpace(defaultPath))
|
||
return absoluteWorkspace; // Kein DefaultPath → absoluten Pfad beibehalten (Legacy)
|
||
|
||
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, comparison))
|
||
return normalizedWorkspace.Substring(normalizedBase.Length + 1);
|
||
|
||
if (normalizedWorkspace.Equals(normalizedBase, comparison))
|
||
return "";
|
||
|
||
return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy)
|
||
}
|
||
}
|