namespace Flaro.Api;
/// Zeigt Ordner des Dateisystems, damit Pfade nicht getippt werden müssen.
///
///
///
/// Warum das überhaupt nötig ist. Ein Pfad in einer Mediathek ist der
/// Pfad im Container, 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.
///
///
/// Nur für Administratoren, und das ist keine Förmlichkeit. Diese
/// Auskunft legt den Aufbau des Dateisystems offen. Im Container ist das
/// wenig, aber eingehängte Ordner tragen die Namen echter Verzeichnisse.
///
///
public static class Ordnerbaum
{
/// Wo die Suche beginnt, wenn niemand einen Pfad nennt.
///
///
/// In dieser Reihenfolge, weil die Anleitung Medien nach /media
/// einhängen lässt. Steht dort nichts, ist die Wurzel der ehrlichste
/// Anfang — raten hilft niemandem.
///
private static readonly string[] Anfänge = ["/media", "/mnt", "/data", "/volume1"];
/// Was an der Wurzel nicht angezeigt wird.
///
///
/// Keine Sicherheitsmassnahme, eine Aufräumarbeit. Wer den Pfad
/// kennt, kann ihn weiterhin eintippen; verborgen wird nichts. Es geht
/// darum, dass eine Liste aus zwanzig Einträgen, von denen achtzehn
/// /proc, /sys und /usr heissen, unbrauchbar ist.
/// Medien liegen dort nie.
///
private static readonly HashSet 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 Entries,
/// Warum die Liste leer ist — oder null, wenn sie es nicht ist.
string? Note);
/// Der Pfad, mit dem die Auswahl aufgeht.
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
: "/";
}
/// Listet die Unterordner eines Verzeichnisses.
///
///
/// Gibt auch dann eine Antwort, wenn das Lesen scheitert. 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. null heisst
/// dagegen: diesen Pfad gibt es nicht.
///
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 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);
}
}