Ein typografisches Anführungszeichen zerriss die Zeichenkette
Beim Einhängen des Ordner-Endpunkts stand „{path}" in einer
interpolierten Zeichenkette. Das schliessende Zeichen beendete sie
mitten im Satz, und der Rest las sich für den Übersetzer als Folge
unbekannter Bezeichner — zehn Fehler aus einer Zeile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a191d0faf
commit
64fb61f2dc
@@ -71,6 +71,24 @@ public static class SystemEndpoints
|
||||
Uptime: (DateTimeOffset.UtcNow - Gestartet).TotalSeconds)))
|
||||
.RequireAuthorization(FlaroPolicies.Administrator);
|
||||
|
||||
// **Ordner zum Durchklicken.** Der Pfad einer Mediathek ist der im
|
||||
// Container, nicht der auf dem NAS — eine Unterscheidung, die nur
|
||||
// kennt, wer die Compose-Datei selbst geschrieben hat. Ein Tippfehler
|
||||
// darin ergibt eine leere Mediathek ohne erkennbaren Grund. Wer sich
|
||||
// durchklickt, sieht stattdessen, was wirklich da ist.
|
||||
//
|
||||
// Das Textfeld daneben bleibt trotzdem: eintippen ist schneller, wenn
|
||||
// man den Pfad kennt, und bei sehr tiefen Bäumen die einzige Geduld
|
||||
// schonende Möglichkeit.
|
||||
app.MapGet("/api/v1/system/folders", (string? path) =>
|
||||
Ordnerbaum.Lesen(path) is { } baum
|
||||
? Results.Ok(baum)
|
||||
: Results.Problem(
|
||||
title: "Ordner nicht gefunden",
|
||||
detail: $"Den Ordner {path} gibt es nicht, oder es ist keiner.",
|
||||
statusCode: StatusCodes.Status404NotFound))
|
||||
.RequireAuthorization(FlaroPolicies.Administrator);
|
||||
|
||||
app.MapPut("/api/v1/system/name", async (ServerNameRequest anfrage, FlaroContext db) =>
|
||||
{
|
||||
var name = anfrage.Name.Trim();
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
namespace Flaro.Api;
|
||||
|
||||
/// <summary>Zeigt Ordner des Dateisystems, damit Pfade nicht getippt werden müssen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Warum das überhaupt nötig ist.</b> Ein Pfad in einer Mediathek ist der
|
||||
/// Pfad <i>im Container</i>, nicht der auf dem NAS — also das, was in der
|
||||
/// Compose-Datei hinter dem Doppelpunkt steht. Diese Unterscheidung kennt
|
||||
/// niemand, der nicht gerade die Compose-Datei geschrieben hat, und ein
|
||||
/// Tippfehler darin ergibt eine leere Mediathek ohne erkennbaren Grund.
|
||||
/// Wer sich durchklicken kann, sieht stattdessen, was wirklich da ist.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Nur für Administratoren, und das ist keine Förmlichkeit.</b> Diese
|
||||
/// Auskunft legt den Aufbau des Dateisystems offen. Im Container ist das
|
||||
/// wenig, aber eingehängte Ordner tragen die Namen echter Verzeichnisse.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class Ordnerbaum
|
||||
{
|
||||
/// <summary>Wo die Suche beginnt, wenn niemand einen Pfad nennt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// In dieser Reihenfolge, weil die Anleitung Medien nach <c>/media</c>
|
||||
/// einhängen lässt. Steht dort nichts, ist die Wurzel der ehrlichste
|
||||
/// Anfang — raten hilft niemandem.
|
||||
/// </remarks>
|
||||
private static readonly string[] Anfänge = ["/media", "/mnt", "/data", "/volume1"];
|
||||
|
||||
/// <summary>Was an der Wurzel nicht angezeigt wird.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Keine Sicherheitsmassnahme, eine Aufräumarbeit.</b> Wer den Pfad
|
||||
/// kennt, kann ihn weiterhin eintippen; verborgen wird nichts. Es geht
|
||||
/// darum, dass eine Liste aus zwanzig Einträgen, von denen achtzehn
|
||||
/// <c>/proc</c>, <c>/sys</c> und <c>/usr</c> heissen, unbrauchbar ist.
|
||||
/// Medien liegen dort nie.
|
||||
/// </remarks>
|
||||
private static readonly HashSet<string> AnDerWurzelVerborgen = new(StringComparer.Ordinal)
|
||||
{
|
||||
"bin", "boot", "dev", "etc", "lib", "lib32", "lib64", "libx32",
|
||||
"proc", "root", "run", "sbin", "sys", "tmp", "usr", "var",
|
||||
};
|
||||
|
||||
public sealed record Eintrag(string Name, string Path);
|
||||
|
||||
public sealed record Antwort(
|
||||
string Path,
|
||||
string? Parent,
|
||||
IReadOnlyList<Eintrag> Entries,
|
||||
/// <summary>Warum die Liste leer ist — oder <c>null</c>, wenn sie es nicht ist.</summary>
|
||||
string? Note);
|
||||
|
||||
/// <summary>Der Pfad, mit dem die Auswahl aufgeht.</summary>
|
||||
public static string Anfang()
|
||||
{
|
||||
foreach (var kandidat in Anfänge)
|
||||
{
|
||||
if (Directory.Exists(kandidat)) return kandidat;
|
||||
}
|
||||
|
||||
// Nicht hart „/": unter Windows und macOS gibt es das so nicht, und
|
||||
// die Tests laufen dort.
|
||||
return Path.GetPathRoot(Environment.CurrentDirectory) is { Length: > 0 } wurzel
|
||||
? wurzel
|
||||
: "/";
|
||||
}
|
||||
|
||||
/// <summary>Listet die Unterordner eines Verzeichnisses.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Gibt auch dann eine Antwort, wenn das Lesen scheitert.</b> Ein
|
||||
/// Ordner ohne Leserecht ist kein Fehler des Aufrufers, sondern eine
|
||||
/// Tatsache über das Dateisystem — und eine, die in der Oberfläche
|
||||
/// stehen soll, statt als 500 im Protokoll zu landen. <c>null</c> heisst
|
||||
/// dagegen: diesen Pfad gibt es nicht.
|
||||
/// </remarks>
|
||||
public static Antwort? Lesen(string? angefragt)
|
||||
{
|
||||
var pfad = string.IsNullOrWhiteSpace(angefragt) ? Anfang() : angefragt.Trim();
|
||||
|
||||
// Macht aus „/media/../etc" wieder „/etc". Das ist keine Abwehr —
|
||||
// Administratoren dürfen überallhin — sondern sorgt dafür, dass der
|
||||
// angezeigte Pfad der ist, den man auch eintippen würde.
|
||||
try { pfad = Path.GetFullPath(pfad); }
|
||||
catch (Exception e) when (e is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(pfad)) return null;
|
||||
|
||||
var eltern = Path.GetDirectoryName(pfad.TrimEnd(Path.DirectorySeparatorChar));
|
||||
var anDerWurzel = string.IsNullOrEmpty(eltern);
|
||||
|
||||
List<Eintrag> eintraege;
|
||||
string? hinweis = null;
|
||||
try
|
||||
{
|
||||
eintraege = Directory.EnumerateDirectories(pfad)
|
||||
.Select(p => new Eintrag(Path.GetFileName(p), p))
|
||||
.Where(e => e.Name.Length > 0)
|
||||
.Where(e => !anDerWurzel || !AnDerWurzelVerborgen.Contains(e.Name))
|
||||
.OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (eintraege.Count == 0) hinweis = "Keine Unterordner.";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
eintraege = [];
|
||||
hinweis = "Kein Leserecht für diesen Ordner.";
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
eintraege = [];
|
||||
hinweis = "Der Ordner liess sich nicht lesen.";
|
||||
}
|
||||
|
||||
return new Antwort(pfad, anDerWurzel ? null : eltern, eintraege, hinweis);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user