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); } }