chapters: Fliesstext auf 57 reine Textseiten kuerzen

Zwei Kompressionsdurchgaenge ueber Kapitel 2-7. Entfernt wurden
Redundanzen, Meta-Kommentare, Ueberklaerungen und Fuellsaetze;
Fakten, Namen, Daten, Entscheidungen samt Begruendung sowie alle
Abbildungen und Tabellen bleiben unveraendert.

Reine Textseiten: 78 -> 57 (Woerter 19613 -> 12451).
Gesamt-PDF: 138 -> 116 Seiten.
This commit is contained in:
2026-08-25 23:02:17 +02:00
parent 1a3d0820da
commit ce0875f29b
38 changed files with 351 additions and 445 deletions
+11 -13
View File
@@ -3,7 +3,7 @@
\subsection{Schichtung}
Das Dokumentenmodul folgt der in Houston bereits etablierten Schichtung und führt keine neuen Strukturmuster ein. Abbildung~\ref{fig:module-components} zeigt die Komponenten und ihre Beziehungen.
Das Dokumentenmodul folgt der in Houston etablierten Schichtung. Abbildung~\ref{fig:module-components} zeigt die Komponenten.
\begin{figure}[H]
\centering
@@ -12,30 +12,28 @@ Das Dokumentenmodul folgt der in Houston bereits etablierten Schichtung und füh
\label{fig:module-components}
\end{figure}
Die oberste Schicht bilden drei Razor Pages. \texttt{Documents} stellt die eigentliche Liste samt Suche, Filtern und Blätterelementen dar. \texttt{Document/Download} nimmt Downloadanforderungen entgegen, sowohl für einzelne Dokumente als auch für ZIP-Archive. \texttt{Document/Share} liefert die Vorschauseite für Freigabelinks. Die Aufteilung auf getrennte Seiten statt auf mehrere Handler einer einzigen Seite folgt daraus, dass Download und Freigabe eigene Routen mit eigenen Antwortformaten benötigen.
Die oberste Schicht bilden drei Razor Pages: \texttt{Documents} für Liste, Suche, Filter und Blätterelemente, \texttt{Document/Download} für Downloadanforderungen (einzeln und als ZIP) und \texttt{Document/Share} für Freigabelinks. Die Aufteilung folgt daraus, dass Download und Freigabe eigene Routen mit eigenen Antwortformaten benötigen.
Darunter liegt der \texttt{DocumentsService} als fachliche Schicht. Er kennt die Regeln des Ablagekonzepts: wie ein Kundenordner aufgelöst wird, welche Objekte als Dokumente gelten, wie der Typ aus dem Pfad abgeleitet wird und welche Einträge auszublenden sind. Der \texttt{S3DocumentsClient} kapselt darunter den technischen Zugriff auf den Speicher und ist die einzige Stelle, an der \texttt{IAmazonS3} unmittelbar verwendet wird.
Darunter liegt der \texttt{DocumentsService} als fachliche Schicht mit den Regeln des Ablagekonzepts: Auflösung des Kundenordners, Typableitung aus dem Pfad, Ausblendung technischer Einträge. Der \texttt{S3DocumentsClient} kapselt den technischen Speicherzugriff und ist die einzige Stelle, an der \texttt{IAmazonS3} unmittelbar verwendet wird.
\subsection{Eigene Typen statt Zeichenketten}
Eine Entwurfsentscheidung, die sich erst im Verlauf der Umsetzung herausbildete, betrifft den Umgang mit Pfaden. In der ersten Fassung wurden Dokumentschlüssel als gewöhnliche Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung — und zwar deshalb, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde.
Eine Entwurfsentscheidung, die sich im Verlauf herausbildete, betrifft den Umgang mit Pfaden. Anfangs wurden Dokumentschlüssel als Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde.
Daraufhin wurden eigene Typen eingeführt, die diese Unterscheidung im Typsystem sichtbar machen. Ein \emph{Schlüssel} bezeichnet den vollständigen, bereits gegen das Kundenpräfix validierten Pfad im Speicher; ein \emph{relativer Pfad} bezeichnet den Anteil unterhalb des Kundenordners, wie er in Freigabelinks und in der ZIP-Struktur auftritt. Beide Typen können nur über Konstruktionswege entstehen, die die jeweilige Prüfung durchführen.
Daraufhin wurden eigene Typen eingeführt: Ein \emph{Schlüssel} bezeichnet den vollständigen, validierten Pfad; ein \emph{relativer Pfad} den Anteil unterhalb des Kundenordners. Beide können nur über Konstruktionswege entstehen, die die jeweilige Prüfung durchführen. Eine vergessene Prüfung führt zu einem Übersetzungsfehler statt zu einer Sicherheitslücke.
Der Nutzen liegt darin, dass eine vergessene Prüfung nicht mehr zu einer Sicherheitslücke, sondern zu einem Übersetzungsfehler führt. Der zugehörige Pull Request durchlief 23 Iterationen und bestand zu einem erheblichen Teil aus genau diesem Refactoring — ein Aufwand, der sich in den folgenden Anforderungen mehrfach auszahlte, weil ZIP-Download, PDF-Vorschau und Freigabelinks dieselben Typen wiederverwenden konnten.
Nach demselben Muster entstanden \texttt{DocumentType} für die Typisierung, \texttt{DocumentName} für die Behandlung von Anzeigenamen und Dateiendungen sowie \texttt{OrgSlug} für die Normalisierung des Firmennamens.
Der zugehörige Pull Request durchlief 23 Iterationen und bestand zu einem erheblichen Teil aus diesem Refactoring — ein Aufwand, der sich auszahlte, weil ZIP-Download, PDF-Vorschau und Freigabelinks dieselben Typen wiederverwenden konnten. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName} und \texttt{OrgSlug}.
\subsection{Zentrale Autorisierung}
Die erste Fassung des Document Explorers prüfte die Berechtigung im PageModel selbst. Im Review wies \emph{Hanna Ebner} darauf hin, dass die Autorisierung in Houston zentral in der Anwendungskonfiguration eingerichtet wird und eine zusätzliche Prüfung an der Seite deshalb überflüssig ist.
Die erste Fassung des Document Explorers prüfte die Berechtigung im PageModel. Im Review wies \emph{Hanna Ebner} darauf hin, dass die Autorisierung in Houston zentral in der Anwendungskonfiguration erfolgt und eine zusätzliche Prüfung an der Seite überflüssig ist.
Der Einwand betrifft mehr als nur doppelten Code. Eine an der Seite hinterlegte Prüfung ist leicht zu übersehen, wenn später eine weitere Seite hinzukommt — genau das wäre bei \texttt{Download} und \texttt{Share} passiert. Die zentrale Registrierung stellt dagegen sicher, dass alle Routen des Moduls derselben Richtlinie unterliegen, ohne dass dies an jeder einzelnen Stelle wiederholt werden muss.
Der Einwand betrifft mehr als doppelten Code: Eine seitenspezifische Prüfung ist leicht zu übersehen, wenn weitere Seiten hinzukommen — genau das wäre bei \texttt{Download} und \texttt{Share} passiert. Die zentrale Registrierung stellt sicher, dass alle Routen des Moduls derselben Richtlinie unterliegen.
\subsection{Gestapelte Pull Requests}
Ein Vorgehen, das sich durch die gesamte Umsetzung zieht und das Verständnis der Reviewhistorie erleichtert, ist die Verkettung der Pull Requests. Der erste Pull Request des Document Explorers ging gegen den Hauptbranch. Der darauf aufbauende Pull Request für die Icons wurde jedoch nicht ebenfalls gegen den Hauptbranch geführt, sondern gegen den Branch des Explorers. Der Suchen-Pull-Request wiederum ging gegen den Icon-Branch, der Download-Pull-Request gegen den Suchen-Branch und der Freigabelink-Pull-Request gegen den Download-Branch.
Ein Vorgehen, das sich durch die gesamte Umsetzung zieht, ist die Verkettung der Pull Requests. Der erste ging gegen den Hauptbranch; jeder darauf aufbauende gegen den Branch seines Vorgängers.
Der Grund ist die Reviewbarkeit. Da die Arbeiten inhaltlich aufeinander aufbauen, hätte ein direkter Vergleich gegen den Hauptbranch bei jedem Pull Request auch sämtliche Änderungen der Vorgänger enthalten. Ein Reviewer hätte die für das jeweilige Backlog Item relevanten Änderungen aus einem stetig wachsenden Gesamtunterschied heraussuchen müssen. Durch die Verkettung enthält jeder Pull Request genau die Änderungen seines eigenen Backlog Items. Auf diesen Umstand wurde jeweils im ersten Kommentar des Pull Requests hingewiesen.
Der Grund ist die Reviewbarkeit. Da die Arbeiten aufeinander aufbauen, hätte ein direkter Vergleich gegen den Hauptbranch auch sämtliche Vorgängeränderungen enthalten. Durch die Verkettung enthält jeder Pull Request genau die Änderungen seines Backlog Items.
Der Preis dieses Vorgehens zeigte sich beim Zusammenführen. Sobald ein Vorgänger in den Hauptbranch übernommen war, musste der Nachfolger auf den Hauptbranch umgestellt werden. Diese Umstellung setzt in Azure DevOps die bereits abgegebenen Freigaben zurück, sodass mehrere Reviewer ein zweites Mal zustimmen mussten. Bei zwei Pull Requests ist dieser Effekt im Ereignisprotokoll dokumentiert. Für künftige Arbeiten wäre abzuwägen, ob der Gewinn an Reviewbarkeit diesen zusätzlichen Abstimmungsaufwand rechtfertigt; bei der vorliegenden Zahl aufeinander aufbauender Backlog Items überwog er deutlich.
Der Preis zeigte sich beim Zusammenführen: Sobald ein Vorgänger in den Hauptbranch übernommen war, musste der Nachfolger umgestellt werden. Diese Umstellung setzt in Azure DevOps die bereits abgegebenen Freigaben zurück. Für künftige Arbeiten wäre abzuwägen, ob der Gewinn an Reviewbarkeit diesen Abstimmungsaufwand rechtfertigt; bei der vorliegenden Zahl aufeinander aufbauender Items überwog er deutlich.
+9 -11
View File
@@ -3,30 +3,28 @@
\subsection{Umfang}
Der Document Explorer war das erste umgesetzte Backlog Item und mit acht Aufwandspunkten zugleich das umfangreichste. Er umfasst die Grundstruktur des gesamten Moduls: die Seite selbst, den Dienst, den Speicherclient, die Modelle sowie die Einbindung in Navigation und Autorisierung. Alle nachfolgenden Backlog Items bauen darauf auf.
Der Document Explorer war das erste umgesetzte Backlog Item und mit acht Aufwandspunkten das umfangreichste. Er umfasst die Grundstruktur des gesamten Moduls: Seite, Dienst, Speicherclient, Modelle sowie Einbindung in Navigation und Autorisierung.
\subsection{Vom Kachel- zum Listenentwurf}
Die ursprüngliche Fassung der Anforderung sah vor, Dokumente als Kacheln darzustellen. Im Verlauf wurde dies auf eine Zeilendarstellung geändert. Ausschlaggebend war weniger eine gestalterische Vorliebe als die absehbare Entwicklung der Anforderungen: Bereits im Backlog standen Auswahlboxen für den ZIP-Download, Download- und Teilen-Schaltflächen sowie eine Vorschaufunktion. Eine Kachel hätte für diese Elemente keinen natürlichen Platz geboten, eine Tabellenzeile dagegen schon. Die Feature-Beschreibung hält ausdrücklich fest, dass die Tabelle flexibel um weitere Spalten erweiterbar sein soll.
Die ursprüngliche Fassung sah eine Kacheldarstellung vor. Im Verlauf wurde auf Zeilendarstellung geändert, da Auswahlboxen für den ZIP-Download, Download- und Teilen-Schaltflächen sowie eine Vorschau im Backlog standen. Eine Kachel hätte für diese Elemente keinen natürlichen Platz geboten. Die Tabelle soll flexibel um weitere Spalten erweiterbar sein.
\subsection{Ableitung des Dokumententyps}
Der Typ eines Dokuments ergibt sich aus dem Namen des Unterordners, in dem es liegt. In der Umsetzung wird der Ordnername gegen eine Aufzählung der bekannten Typen abgeglichen. Im Review schlug \emph{Timo Walter} vor, hierfür die Zeichenketten-Umwandlung der Aufzählung mit Nichtbeachtung der Groß- und Kleinschreibung zu verwenden, statt eine eigene Zuordnung zu pflegen.
Der Typ eines Dokuments ergibt sich aus dem Unterordnernamen und wird gegen eine Aufzählung abgeglichen. Im Review schlug \emph{Timo Walter} vor, die Zeichenketten-Umwandlung mit Nichtbeachtung der Groß- und Kleinschreibung zu verwenden statt einer eigenen Zuordnung. Beim Hinzufügen eines Typs muss so nur die Aufzählung ergänzt werden. Schlägt die Umwandlung fehl, gilt das Dokument als untypisiert.
Der Vorschlag wurde übernommen. Er hat den Vorteil, dass beim Hinzufügen eines Typs nur die Aufzählung ergänzt werden muss und die Zuordnung nicht an einer zweiten Stelle nachgeführt werden kann. Schlägt die Umwandlung fehl, liefert die Methode kein Ergebnis, und das Dokument gilt als untypisiert — genau das gewünschte Verhalten für unbekannte Ordner.
Eine bewusste Ausnahme betrifft die Übersetzung der Typbezeichnungen. Fehlt für einen bekannten Typ der Eintrag in der Ressourcendatei, wird kein Ersatzwert verwendet, sondern eine Ausnahme ausgelöst. Im Review wurde hinterfragt, ob das nicht ein zulässiger Fall sei. Die Begründung dagegen: Jeder Wert der Aufzählung ist per Definition gültig; fehlt seine Übersetzung, ist das kein Laufzeitzustand, sondern eine unvollständige Implementierung. Ein stiller Rückfall auf den technischen Ordnernamen würde diesen Fehler verdecken, statt ihn sichtbar zu machen.
Eine bewusste Ausnahme betrifft die Übersetzung: Fehlt für einen bekannten Typ der Eintrag in der Ressourcendatei, wird eine Ausnahme ausgelöst. Jeder Wert der Aufzählung ist per Definition gültig; fehlt seine Übersetzung, ist das eine unvollständige Implementierung. Ein stiller Rückfall auf den Ordnernamen würde diesen Fehler verdecken.
\subsection{Leerer Zustand und Fehlerbehandlung}
Zwei Anforderungen betreffen Situationen, in denen keine Dokumente angezeigt werden können, und sie sind ausdrücklich voneinander zu unterscheiden.
Zwei Anforderungen betreffen Situationen ohne anzeigbare Dokumente und sind zu unterscheiden.
Der \textbf{leere Zustand} tritt ein, wenn der Kunde berechtigt ist, aber noch keine Dokumente für ihn abgelegt wurden. Er ist ein normaler Betriebszustand und wird mit einem entsprechenden Hinweis dargestellt. Dieser Fall tritt insbesondere unmittelbar nach der automatischen Anlage der Ordnerstruktur auf.
Der \textbf{leere Zustand} tritt ein, wenn der Kunde berechtigt ist, aber keine Dokumente vorliegen — insbesondere unmittelbar nach der automatischen Ordneranlage. Er wird mit einem Hinweis dargestellt.
Ein \textbf{Fehler} liegt dagegen vor, wenn die Dokumente nicht geladen werden konnten — etwa weil der Speicher nicht erreichbar ist oder die Zugangsdaten nicht stimmen. Im Review wurde ausdrücklich angemerkt, dass dieser Fall nicht in einer stillschweigend leeren Seite münden darf. Der Unterschied ist für den Kunden erheblich: Im einen Fall gibt es nichts zu sehen, im anderen funktioniert die Anwendung nicht. Würden beide Fälle gleich dargestellt, bliebe eine Störung unbemerkt, bis jemand sie zufällig meldet.
Ein \textbf{Fehler} liegt vor, wenn die Dokumente nicht geladen werden konnten. Im Review wurde angemerkt, dass dieser Fall nicht in einer stillschweigend leeren Seite münden darf: Im einen Fall gibt es nichts zu sehen, im anderen funktioniert die Anwendung nicht.
\subsection{Die bekannte Schwäche}
Bereits im Review dieses Pull Requests fragte \emph{Hanna Ebner}, warum für jeden Kunden eine eigene Abfrage abgesetzt werde und ob sich das nicht über einen Filter lösen lasse. Die Antwort war, dass die Schnittstelle dies nicht zulässt, verbunden mit dem Verweis auf das eigens angelegte Recherche-Item.
Bereits im Review fragte \emph{Hanna Ebner}, warum für jeden Kunden eine eigene Abfrage nötig sei. Die Antwort war, dass die Schnittstelle dies nicht anders zulässt, verbunden mit dem Verweis auf das eigens angelegte Recherche-Item.
Diese Stelle ist aus zwei Gründen bemerkenswert. Erstens zeigt sie, dass die Schwäche nicht übersehen, sondern erkannt und bewusst zurückgestellt wurde: Der Explorer sollte nicht daran scheitern, dass eine Optimierung noch nicht gefunden war. Zweitens dokumentiert der Verweis auf das Folgeitem die Entscheidung nachvollziehbar — der Reviewer musste sie nicht glauben, sondern konnte sie im Backlog nachvollziehen. Die tatsächliche Lösung dieses Problems beschreibt Abschnitt~\ref{sec:folder-management}.
Die Schwäche wurde erkannt und bewusst zurückgestellt — der Explorer sollte nicht daran scheitern, dass eine Optimierung noch nicht gefunden war. Die tatsächliche Lösung beschreibt Abschnitt~\ref{sec:folder-management}.
+6 -16
View File
@@ -3,19 +3,15 @@
\subsection{Einzeldownload über zeitlich begrenzte Zugriffs-URLs}
Für den Download einzelner Dokumente wäre es möglich gewesen, den Inhalt aus dem Speicher zu lesen und durch die Anwendung hindurch an den Browser weiterzureichen. Stattdessen wird eine zeitlich begrenzte Zugriffs-URL erzeugt, mit der der Browser das Dokument unmittelbar vom Speicher abruft \autocite{aws-presigned-urls}.
Für den Einzeldownload wird eine zeitlich begrenzte Zugriffs-URL erzeugt, mit der der Browser das Dokument unmittelbar vom Speicher abruft \autocite{aws-presigned-urls}. Die Datenübertragung läuft nicht über die Anwendung; ein großes Dokument belegt weder Arbeitsspeicher noch Verbindungen des Webservers. Die URL ist nur wenige Minuten gültig.
Der Vorteil liegt darin, dass die eigentliche Datenübertragung nicht über die Anwendung läuft. Ein großes Dokument belegt weder Arbeitsspeicher noch eine Verbindung des Webservers; die Anwendung ist nach dem Erzeugen der URL nicht mehr beteiligt. Die URL ist nur wenige Minuten gültig und danach wertlos.
Wichtig ist, dass die Berechtigungsprüfung \emph{vor} dem Erzeugen der URL stattfindet. Die URL selbst trägt keine Prüfung — wer sie besitzt, kann zugreifen. Sie darf deshalb nur für einen Schlüssel erzeugt werden, der nachweislich im Kundenordner liegt. Genau hier setzte der überwiegende Teil des Reviews an, und die Rückfragen führten zur in Abschnitt~\ref{sec:architecture} beschriebenen Einführung eigener Pfadtypen.
Wichtig ist dabei, dass die Berechtigungsprüfung \emph{vor} dem Erzeugen der URL stattfindet. Die URL selbst trägt keine Prüfung mehr in sich — wer sie besitzt, kann innerhalb ihrer Gültigkeitsdauer auf das Dokument zugreifen. Sie darf deshalb nur für einen Schlüssel erzeugt werden, der nachweislich innerhalb des Kundenordners liegt.
Genau an dieser Stelle setzte der überwiegende Teil des Reviews an. Die Prüfung, dass ein angeforderter Pfad den Kundenordner nicht verlässt, war die inhaltlich kritischste Einzelanforderung des Moduls, und die wiederholten Rückfragen dazu führten zu der in Abschnitt~\ref{sec:architecture} beschriebenen Einführung eigener Pfadtypen. Der Pull Request durchlief 23 Iterationen; ein erheblicher Anteil davon entfiel auf dieses Refactoring und nicht auf die Downloadfunktion selbst.
Zusätzlich musste sichergestellt werden, dass das Dokument unter seinem ursprünglichen Namen ankommt. Da der Schlüssel im Speicher den vollständigen Pfad enthält, würde ein unbehandelter Download unter einem technischen Namen gespeichert. Der gewünschte Dateiname wird deshalb ausdrücklich mitgegeben.
Zusätzlich musste sichergestellt werden, dass das Dokument unter seinem ursprünglichen Namen ankommt, da der Schlüssel den vollständigen Pfad enthält.
\subsection{ZIP-Download}
Für den Download mehrerer Dokumente erhält jede Zeile eine Auswahlbox. Die Schaltfläche für den ZIP-Download ist deaktiviert, solange nichts ausgewählt ist, und wird erst mit der ersten Auswahl freigegeben. Damit ist die Anforderung, dass ohne Auswahl kein Download ausgelöst werden kann, unmittelbar in der Bedienoberfläche abgebildet und muss nicht durch eine Fehlermeldung nachgereicht werden.
Jede Zeile erhält eine Auswahlbox für den ZIP-Download. Die Schaltfläche ist deaktiviert, solange nichts ausgewählt ist.
Abbildung~\ref{fig:zip-stream} zeigt den Ablauf.
@@ -26,14 +22,8 @@ Abbildung~\ref{fig:zip-stream} zeigt den Ablauf.
\label{fig:zip-stream}
\end{figure}
Das Archiv wird erst beim Klick erzeugt und nicht im Voraus vorgehalten — auch dies eine ausdrückliche Anforderung. Entscheidend für die Umsetzung ist, dass es dabei zu keinem Zeitpunkt vollständig im Arbeitsspeicher oder auf der Festplatte entsteht: Die Anwendung öffnet die Antwort als Archivstrom, liest die ausgewählten Dokumente nacheinander aus dem Speicher und schreibt sie unmittelbar als Einträge hinein \autocite{dotnet-zip}. Die komprimierten Daten fließen dabei direkt zum Browser.
Diese Arbeitsweise ist notwendig, weil die Gesamtgröße einer Auswahl nicht begrenzt ist. Würde das Archiv zunächst vollständig aufgebaut, bestimmte die größte denkbare Auswahl den Speicherbedarf des Servers. Beim Strömen bleibt dieser dagegen unabhängig von der Auswahlgröße konstant. Die Struktur innerhalb des Archivs entspricht der Ablagestruktur, sodass die Typordner erhalten bleiben.
Das Archiv wird erst beim Klick erzeugt. Entscheidend ist, dass es nie vollständig im Arbeitsspeicher entsteht: Die Anwendung öffnet die Antwort als Archivstrom, liest die Dokumente nacheinander aus dem Speicher und schreibt sie unmittelbar als Einträge hinein \autocite{dotnet-zip}. Da die Gesamtgröße unbegrenzt ist, würde ein vollständiger Aufbau den Speicherbedarf von der größten Auswahl abhängig machen. Beim Strömen bleibt er konstant.
\subsection{ZIP auch bei einer einzelnen Datei}
Im Review stellte \emph{Sarah Hinzmann} die Frage, ob auch bei nur einer ausgewählten Datei ein Archiv erzeugt werden solle, statt die Datei direkt auszuliefern.
Die Entscheidung fiel für das Archiv. Der Grund ist die Vorhersagbarkeit der Bedienung: Die Schaltfläche ist mit „Als ZIP herunterladen" beschriftet, und wer sie betätigt, soll ein Archiv erhalten — unabhängig davon, wie viele Dokumente ausgewählt sind. Wer eine einzelne Datei unverpackt benötigt, verwendet die Download-Schaltfläche in der jeweiligen Zeile. Eine automatische Umschaltung des Verhaltens abhängig von der Auswahlgröße wäre für den Benutzer schwerer zu antizipieren und hätte zudem bedeutet, dass ein Skript auf der Seite die beiden Fälle unterscheiden müsste.
Der Fall zeigt beispielhaft, wie eine scheinbar kleine Bedienfrage eine begründete Entscheidung erfordert. Beide Varianten sind vertretbar; ausschlaggebend war, dass jede Schaltfläche genau eine Bedeutung behält.
Im Review stellte \emph{Sarah Hinzmann} die Frage, ob bei nur einer ausgewählten Datei ein Archiv erzeugt werden solle. Die Entscheidung fiel für das Archiv: Die Schaltfläche ist mit „Als ZIP herunterladen" beschriftet und soll vorhersagbar ein Archiv liefern. Wer eine einzelne Datei unverpackt benötigt, verwendet die Download-Schaltfläche in der Zeile.
+6 -14
View File
@@ -5,28 +5,20 @@
Unterhalb der Suchleiste steht je Dokumententyp ein Bedienelement zum Ein- und Ausblenden. Die Auswahl wird als Abfrageparameter übertragen und im PageModel ausgewertet; die Filterung selbst erfolgt anschließend im Dienst.
Die Auswertung des Parameters war Gegenstand des Reviews. Da die Werte aus der Adresszeile stammen, muss die Anwendung damit rechnen, dass sie manipuliert oder veraltet sind — etwa wenn ein gespeicherter Link auf einen inzwischen entfernten Typ verweist. \emph{Robin Noack} schlug vor, die Umwandlung fehlertolerant vorzunehmen und nicht erkennbare Werte stillschweigend zu verwerfen, statt die Anfrage scheitern zu lassen.
Die Auswertung des Parameters war Gegenstand des Reviews. Da die Werte aus der Adresszeile stammen, muss die Anwendung mit manipulierten oder veralteten Werten rechnen. \emph{Robin Noack} schlug vor, nicht erkennbare Werte stillschweigend zu verwerfen. Ein unbekannter Filterwert ist kein Angriff, sondern in der Regel ein veralteter Link. Die Mandantentrennung geschieht über das Präfix, nicht über den Filter.
Die Begründung überzeugt: Ein unbekannter Filterwert ist kein Angriff und kein Fehler des Benutzers, sondern in aller Regel ein veralteter Link. Die Anwendung ignoriert ihn und zeigt die verbleibenden Typen an. Ein Fehlerbild wäre hier unangemessen, zumal keine Sicherheitsentscheidung von diesem Wert abhängt — die Mandantentrennung geschieht über das Präfix, nicht über den Filter.
Ein zweiter Reviewhinweis betraf die Benennung. Der Typ für eine Menge von Dokumententypen unterschied sich vom Typ für einen einzelnen Dokumententyp lediglich durch ein angehängtes Zeichen. Da eine Verwechslung beim Lesen leicht möglich und beim Übersetzen nicht auffällig ist, wurde die Benennung geschärft.
Ein zweiter Reviewhinweis betraf die Benennung: Der Typ für eine Menge von Dokumententypen unterschied sich vom Einzeltyp nur durch ein Zeichen. Da eine Verwechslung leicht möglich ist, wurde die Benennung geschärft.
\subsection{Paginierung mit Fortsetzungsmerkmalen}
Die Paginierung folgt der Arbeitsweise des Speichers. Dieser liefert Ergebnisse blockweise und gibt, sofern weitere Treffer vorliegen, ein Fortsetzungsmerkmal zurück, mit dem der jeweils nächste Block angefordert werden kann \autocite{aws-listobjectsv2}.
Dieses Verfahren unterscheidet sich grundlegend von der sonst üblichen Paginierung über eine Positionsangabe. Ein Fortsetzungsmerkmal beschreibt keine Position im Ergebnis, sondern setzt die Auflistung an der Stelle fort, an der sie zuletzt endete. Daraus folgt eine Einschränkung, die sich nicht umgehen lässt: Ein direkter Sprung auf eine beliebige Seite ist nicht möglich, ohne alle vorhergehenden Blöcke abzurufen. Die Akzeptanzkriterien verlangten dies auch nicht — gefordert waren eine Vor- und eine Zurück-Navigation.
Die Paginierung folgt der Arbeitsweise des Speichers, der Ergebnisse blockweise mit Fortsetzungsmerkmalen liefert \autocite{aws-listobjectsv2}. Ein Fortsetzungsmerkmal setzt die Auflistung dort fort, wo sie endete. Ein direkter Sprung auf eine beliebige Seite ist nicht möglich, ohne alle vorhergehenden Blöcke abzurufen. Die Akzeptanzkriterien verlangten dies auch nicht — gefordert waren Vor- und Zurück-Navigation.
\subsection{Übernommene Annahmen}
Ein aufschlussreicher Reviewfund betraf einen Zahlenwert für die maximale Seitengröße. Er war aus den bestehenden Houston-Seiten übernommen worden, um die geforderte Konsistenz herzustellen (NFA-4). \emph{Robin Noack} wies darauf hin, dass dieser Wert dort aus einer Beschränkung des ITSM-Systems stammt und für den Speicher keine Bedeutung hat.
Ein Zahlenwert für die maximale Seitengröße war aus den bestehenden Houston-Seiten übernommen worden (NFA-4). \emph{Robin Noack} wies darauf hin, dass dieser Wert dort aus einer Beschränkung des ITSM-Systems stammt und für den Speicher keine Bedeutung hat. Was wie eine Konvention aussah, war eine technische Grenze eines anderen Systems. Der Wert wurde angepasst.
Der Fund ist typisch für das Übernehmen bestehender Muster: Was wie eine Konvention aussah, war tatsächlich eine technische Grenze eines ganz anderen Systems. Konsistenz in der Bedienung bedeutet nicht, dass auch die zugrunde liegenden technischen Werte zu übernehmen sind. Der Wert wurde an die Gegebenheiten des Speichers angepasst.
Diskutiert wurde in diesem Zusammenhang auch, ob die vom Benutzer wählbare Seitengröße nach oben begrenzt werden sollte. Da der Wert aus der Adresszeile stammt, könnte ein sehr großer Wert eine entsprechend große Antwort erzwingen.
Diskutiert wurde auch, ob die vom Benutzer wählbare Seitengröße nach oben begrenzt werden sollte, da ein sehr großer Wert aus der Adresszeile eine entsprechend große Antwort erzwingen könnte.
\subsection{Zusammenspiel mit Freigabelinks}
Die Paginierung wirkt sich unmittelbar auf die Freigabelinks aus. Ein Link, der auf ein bestimmtes Dokument verweist, ist wertlos, wenn er stets auf der ersten Seite landet und das Dokument auf der vierten liegt.
Die Weiterleitung eines Freigabelinks setzt deshalb nicht nur die Sprungmarke auf das Dokument, sondern auch die Abfrageparameter, die zur richtigen Seite führen. Diese Anforderung war bereits bei der Formulierung des Paginierungs-Items berücksichtigt worden — ein Hinweis darauf, dass die wechselseitigen Abhängigkeiten der nachgeschobenen Items zu diesem Zeitpunkt bereits im Blick waren.
Die Paginierung wirkt sich auf die Freigabelinks aus: Ein Link auf ein Dokument ist wertlos, wenn er stets auf der ersten Seite landet. Die Weiterleitung setzt deshalb nicht nur die Sprungmarke, sondern auch die Abfrageparameter für die richtige Seite. Diese Abhängigkeit war bei der Formulierung des Items berücksichtigt worden.
+10 -16
View File
@@ -3,36 +3,30 @@
\subsection{Gemeinsame Umsetzung zweier Backlog Items}
Die automatische Anlage der Ordnerstruktur und der neue Kundenordner-Lookup wurden ursprünglich als getrennte Backlog Items geführt und in einem gemeinsamen Pull Request umgesetzt. Der Grund ist in der Beschreibung des Pull Requests festgehalten: Der Lookup benötigt in seiner Rückfallebene ohnehin den Fall, dass kein Ordner existiert und die Struktur angelegt werden muss. Die Ordneranlage getrennt zu implementieren hätte bedeutet, an dieser Stelle zunächst einen Platzhalter einzufügen, um ihn im unmittelbar folgenden Pull Request wieder zu entfernen.
Die Zusammenlegung wurde nicht stillschweigend vorgenommen, sondern in der Beschreibung begründet und beide Items wurden verknüpft. Damit bleibt der Zusammenhang zwischen Planung und Umsetzung nachvollziehbar, auch wenn die Aufteilung nicht eingehalten wurde.
Die automatische Ordneranlage und der neue Kundenordner-Lookup wurden in einem gemeinsamen Pull Request umgesetzt. Der Lookup benötigt in seiner Rückfallebene den Fall, dass kein Ordner existiert und die Struktur angelegt werden muss. Eine getrennte Implementierung hätte einen Platzhalter erfordert, der im nächsten Pull Request entfernt worden wäre. Die Zusammenlegung wurde begründet und beide Items verknüpft.
\subsection{Normalisierung des Firmennamens}
Der erwartete Ordnername entsteht aus dem Firmennamen des angemeldeten Benutzers. Da dieser Name aus einem Fremdsystem stammt und beliebige Zeichen enthalten kann, wird er zuvor normalisiert. Die Normalisierung entfernt umschließende Leerzeichen, ersetzt Zeichen, die im Schlüssel eine strukturelle Bedeutung haben oder nicht darstellbar sind, verhindert führende und abschließende Punkte und begrenzt die Länge.
Der erwartete Ordnername entsteht aus dem Firmennamen des Benutzers. Da dieser aus einem Fremdsystem stammt und beliebige Zeichen enthalten kann, wird er normalisiert: umschließende Leerzeichen entfernt, strukturell bedeutsame oder nicht darstellbare Zeichen ersetzt, führende und abschließende Punkte verhindert, die Länge begrenzt.
Zwei Eigenschaften sind dabei entscheidend. Die Normalisierung ist \textbf{deterministisch} — derselbe Eingabename ergibt stets denselben Ordnernamen, was die Voraussetzung dafür ist, dass der Direktzugriff überhaupt funktionieren kann. Und sie erhält \textbf{Umlaute}, weil der Ordner in Filestash von Menschen gelesen wird; die Zumutung, eine Firma „Müller GmbH" unter \texttt{Mueller GmbH} zu suchen, wäre der Bedienbarkeit abträglich.
Zwei Eigenschaften sind entscheidend. Die Normalisierung ist \textbf{deterministisch} — derselbe Eingabename ergibt stets denselben Ordnernamen, Voraussetzung für den Direktzugriff. Sie erhält \textbf{Umlaute}, weil der Ordner in Filestash von Menschen gelesen wird.
Im Review fragte \emph{Robin Noack}, ob die gewählten Einschränkungen ausreichen. Die Frage ist berechtigt, denn eine zu schwache Normalisierung führt zu Schlüsseln, die der Speicher zurückweist oder anders interpretiert als erwartet — und beides wäre nicht bei der Entwicklung, sondern erst bei einem Kunden mit einem ungewöhnlichen Firmennamen aufgefallen. Die Antwort verwies auf die Herstellerdokumentation zu den Namensbeschränkungen \autocite{ibm-s3-naming, aws-s3-naming}. Der Punkt ist erwähnenswert, weil er zeigt, dass eine solche Zusicherung sich belegen lassen muss und nicht auf einer Vermutung beruhen darf.
Im Review fragte \emph{Robin Noack}, ob die Einschränkungen ausreichen. Eine zu schwache Normalisierung führt zu Schlüsseln, die der Speicher zurückweist — das wäre erst bei einem Kunden mit ungewöhnlichem Firmennamen aufgefallen. Die Antwort verwies auf die Herstellerdokumentation \autocite{ibm-s3-naming, aws-s3-naming}.
\subsection{Umsetzung des Lookups}
Die in Abschnitt~\ref{sec:lookup-decision} beschriebene dreistufige Auflösung wurde in einer Methode des Dienstes zusammengefasst. Sie ist die einzige Stelle, an der ein Kundenpräfix entsteht; alle übrigen Zugriffe erhalten das Ergebnis als bereits aufgelösten Wert.
Die in Abschnitt~\ref{sec:lookup-decision} beschriebene dreistufige Auflösung wurde in einer Methode zusammengefasst — der einzigen Stelle, an der ein Kundenpräfix entsteht. Der erste Schritt fragt die Metadaten des erwarteten Markers ab. Der zweite wiederholt dies mit dem um die Organisations-ID ergänzten Namen (Kollisionsfall). Erst der dritte greift auf das lineare Verfahren zurück.
Der erste Schritt fragt die Metadaten des erwarteten Markers ab. Ist er vorhanden und trägt die Organisations-ID des Benutzers, endet die Auflösung. Der zweite Schritt wiederholt dies mit dem um die Organisations-ID ergänzten Namen und deckt damit den Kollisionsfall ab. Erst der dritte Schritt greift auf das ursprüngliche, lineare Verfahren zurück.
Beim Prüfen, ob ein Zielname beansprucht werden darf, wird nicht nur der Marker betrachtet, sondern zusätzlich geprüft, ob unterhalb des Zielpräfixes bereits Objekte liegen. Dazu genügt eine Auflistung, die nach dem ersten Treffer abbricht. Ein Zielname gilt nur dann als frei, wenn dort weder ein fremder Marker liegt noch bereits Inhalte vorhanden sind.
Beim Prüfen, ob ein Zielname frei ist, wird nicht nur der Marker betrachtet, sondern auch geprüft, ob bereits Objekte unterhalb des Zielpräfixes liegen. Ein Zielname gilt nur als frei, wenn weder ein fremder Marker noch Inhalte vorhanden sind.
\subsection{Umbenennen als Kopiervorgang}
Der Speicher kennt keine Umbenennung. Ein Objekt lässt sich nur unter einem neuen Schlüssel kopieren und anschließend unter dem alten löschen. Für einen Ordner mit vielen Dokumenten bedeutet das entsprechend viele Einzelvorgänge.
Der Speicher kennt keine Umbenennung. Ein Objekt lässt sich nur kopieren und anschließend löschen. Eine Umbenennung ist daher nicht unteilbar — sie kann abbrechen, und Objekte liegen teils am alten, teils am neuen Ort.
Daraus folgt, dass eine Umbenennung nicht unteilbar ist. Sie kann in der Mitte abbrechen — etwa durch einen Netzwerkfehler —, und dann liegen die Objekte teils am alten, teils am neuen Ort. Die Akzeptanzkriterien verlangten für diesen Fall ausdrücklich, dass keine Dokumente verloren gehen.
Umgesetzt wurde dies über zwei Maßnahmen. Erstens wird der \textbf{Marker zuletzt kopiert}: Solange er fehlt, wird das Ziel von einer parallelen Anfrage nicht als Kundenordner erkannt. Zweitens entfernt die Fehlerbehandlung bereits erzeugte Teilkopien, damit der Vorgang wiederholbar ist.
Umgesetzt wurde dies über zwei Maßnahmen. Erstens wird der \textbf{Marker zuletzt kopiert}. Solange er fehlt, gilt das Ziel als unvollständig und wird von einer parallelen Anfrage nicht als bestehender Kundenordner erkannt. Der Zielordner wird also erst in dem Moment „gültig", in dem er vollständig ist. Zweitens versucht die Fehlerbehandlung, die bereits erzeugten Teilkopien wieder zu entfernen, damit das Ziel leer bleibt und der Vorgang wiederholbar ist.
Die zweite Maßnahme beruht auf einer Annahme, die sich später als nicht allgemeingültig erwies — sie gilt nur, solange kein zweiter Vorgang dieselbe Umbenennung ausführt. Dieser Fall ist Gegenstand des folgenden Abschnitts.
Die zweite Maßnahme gilt nur, solange kein zweiter Vorgang dieselbe Umbenennung ausführt. Dieser Fall ist Gegenstand des folgenden Abschnitts.
\subsection{Stand bei Abgabe}
Der zugehörige Pull Request war zum Zeitpunkt der Erstellung dieser Dokumentation noch offen. Die inhaltlichen Anmerkungen aus dem Review waren geschlossen, die abschließende Freigabe stand jedoch aus. Der Abschnitt beschreibt damit — anders als die vorangegangenen — Arbeit, die fachlich fertiggestellt, aber noch nicht in den Hauptbranch übernommen war.
Der zugehörige Pull Request war zum Zeitpunkt der Erstellung dieser Dokumentation noch offen. Die inhaltlichen Anmerkungen waren geschlossen, die abschließende Freigabe stand aus.
+9 -13
View File
@@ -3,30 +3,26 @@
\subsection{PDF-Vorschau im Modal}
Die Vorschau öffnet PDF-Dokumente in einem überlagerten Fenster, ohne dass sie zuvor heruntergeladen werden müssen. Sie greift dabei auf dieselbe zeitlich begrenzte Zugriffs-URL zurück, die auch dem Download zugrunde liegt — der Unterschied liegt allein darin, dass der Browser das Dokument anzeigt, statt es zu speichern. Für Dateien, die keine PDF-Dokumente sind, wird keine Vorschau angeboten.
Die Vorschau öffnet PDF-Dokumente in einem überlagerten Fenster über dieselbe zeitlich begrenzte Zugriffs-URL, die auch dem Download zugrunde liegt. Für Nicht-PDF-Dateien wird keine Vorschau angeboten.
Bewusst nicht umgesetzt wurde eine Prüfung der Dateigröße. Diese Abgrenzung wurde im Approval-Termin ausdrücklich in die Anforderung aufgenommen: Das Dokument wird angezeigt, unabhängig davon, wie groß es ist. Die Begründung ist pragmatisch — jede Größengrenze wäre willkürlich, und die Betrachtung eines großen Dokuments ist kein Fehlerfall, sondern lediglich langsam. Der Browser stellt PDF-Dokumente ohnehin fortlaufend dar.
Eine Prüfung der Dateigröße wurde bewusst nicht umgesetzt. Diese Abgrenzung wurde im Approval-Termin festgelegt: Jede Grenze wäre willkürlich, und der Browser stellt PDF-Dokumente ohnehin fortlaufend dar.
\subsection{Aufbau der Freigabelinks}
Ein Freigabelink verweist auf eine eigene Seite, deren Adresse den relativen Pfad des Dokuments in kodierter Form enthält. Kodiert wird dabei nur der Anteil unterhalb des Kundenordners, nicht der Kundenordner selbst. Das ist keine Sicherheitsmaßnahme — die Kodierung ist trivial umkehrbar —, sondern dient dazu, Pfadtrennzeichen und Sonderzeichen unbeschadet durch die Adresse zu transportieren.
Ein Freigabelink verweist auf eine eigene Seite, deren Adresse den relativen Pfad des Dokuments kodiert enthält. Kodiert wird nur der Anteil unterhalb des Kundenordners, um Pfadtrennzeichen und Sonderzeichen unbeschadet durch die Adresse zu transportieren.
Dass der Kundenordner nicht Bestandteil des Links ist, hat dagegen sehr wohl eine Wirkung: Der Link enthält keinen Hinweis auf die Organisation und ist ohne den Kontext des angemeldeten Benutzers nicht auflösbar. Die Zuordnung geschieht ausschließlich über die Anmeldung des Aufrufers.
Dass der Kundenordner nicht Bestandteil des Links ist, hat eine Wirkung: Der Link enthält keinen Hinweis auf die Organisation und ist ohne Anmeldung nicht auflösbar.
Die Seite selbst zeigt keinen Dokumentinhalt an. Sie stellt Metaangaben für die Vorschau bereit und leitet anschließend auf die Dokumentenliste weiter, wobei die Sprungmarke auf das jeweilige Dokument gesetzt und — nach Umsetzung der Paginierung — die passende Seite angesteuert wird.
Die Seite zeigt keinen Dokumentinhalt an. Sie liefert Metaangaben für die Vorschau und leitet auf die Dokumentenliste weiter, wobei Sprungmarke und passende Seite angesteuert werden.
\subsection{Vorschau in Messengern}
Der eigentliche Zweck der Freigabeseite ist, dass ein in einem Messenger geteilter Link dort mit Titel und Symbol dargestellt wird statt als nackte Adresse. Dazu werden entsprechende Metaangaben ausgeliefert: der Dokumentname als Titel, das Typsymbol als Bild und die Adresse der Dokumentenliste als Ziel.
Der Zweck der Freigabeseite ist, dass ein in einem Messenger geteilter Link mit Titel und Symbol dargestellt wird. Dazu werden Metaangaben ausgeliefert: Dokumentname als Titel, Typsymbol als Bild, Adresse der Dokumentenliste als Ziel.
Hier trat ein Problem auf, das sich nicht aus der Spezifikation ableiten ließ. Die Typsymbole liegen als Vektorgrafiken vor, und diese werden von den Vorschaudiensten gängiger Messenger nicht zuverlässig dargestellt. Die Symbole mussten daher zusätzlich als Rastergrafiken bereitgestellt werden, allein für diesen Zweck.
Der Umstand hat eine unangenehme Nebenwirkung: Jedes Symbol existiert nun in zwei Formaten, die bei einer Gestaltungsänderung gemeinsam nachzuziehen sind. Im Pull Request wurde ausdrücklich vermerkt, dass die Rastergrafiken zu aktualisieren sind, sobald das Symboldesign endgültig feststeht.
Die Typsymbole liegen als Vektorgrafiken vor, die von Vorschaudiensten gängiger Messenger nicht zuverlässig dargestellt werden. Sie mussten daher zusätzlich als Rastergrafiken bereitgestellt werden. Im Pull Request wurde vermerkt, dass die Rastergrafiken bei einer Gestaltungsänderung nachzuziehen sind.
\subsection{Sichtbarkeit der Vorschaubilder}
Eine Entscheidung, die erst bei der Unterstützung von URL-Dateien vollständig zum Tragen kam, betrifft die Auslieferung der Vorschaubilder. Damit ein Messenger eine Vorschau erzeugen kann, muss er das Bild abrufen können — und zwar ohne Anmeldung, da der Vorschaudienst des Messengers keine Sitzung des Benutzers besitzt. Das Bild liegt somit auf einem öffentlich erreichbaren Pfad.
Damit ein Messenger eine Vorschau erzeugen kann, muss er das Bild ohne Anmeldung abrufen können. Bei den sieben festen Typsymbolen ist das unbedenklich, da sie keine kundenbezogene Information enthalten. Benutzerdefinierte Symbole aus URL-Dateien werden daher \emph{nicht} für die Vorschau ausgeliefert — andernfalls müsste Dateiinhalt über einen nicht authentifizierten Pfad zugänglich gemacht werden.
Solange es sich um die sieben festen Typsymbole handelt, ist das unbedenklich: Sie sind Bestandteil der Anwendung und enthalten keine kundenbezogene Information. Anders verhielte es sich bei Symbolen, die aus dem Inhalt einer Datei stammen. Aus diesem Grund wurde festgelegt, dass benutzerdefinierte Symbole aus URL-Dateien \emph{nicht} für die Vorschau ausgeliefert werden — andernfalls müsste Dateiinhalt über einen nicht authentifizierten Pfad zugänglich gemacht werden.
Die Entscheidung wurde im Pull Request ausdrücklich festgehalten. Sie ist ein Beispiel dafür, dass eine an sich harmlose Funktion — ein Vorschaubild — in Verbindung mit einer anderen Funktion eine Sicherheitsfrage aufwirft, die keine der beiden für sich genommen aufgeworfen hätte.
Die Entscheidung wurde im Pull Request festgehalten. Sie zeigt, dass eine harmlose Funktion in Verbindung mit einer anderen eine Sicherheitsfrage aufwirft, die keine der beiden für sich genommen aufgeworfen hätte.
+15 -23
View File
@@ -3,15 +3,13 @@
\subsection{Ausgangslage}
Der Lesepfad des Kundenordner-Lookups ist unkritisch. Er fragt Metadaten ab und listet Objekte auf; gleichzeitige Aufrufe stören einander nicht. Der in Abschnitt~\ref{sec:folder-management} beschriebene dritte Schritt ist jedoch \emph{verändernd}: Er benennt Ordner um und legt sie an. Und er tut dies im Rahmen einer gewöhnlichen Seitenanfrage.
Der Lesepfad des Kundenordner-Lookups ist unkritisch. Der in Abschnitt~\ref{sec:folder-management} beschriebene dritte Schritt ist jedoch \emph{verändernd}: Er benennt Ordner um und legt sie an, im Rahmen einer gewöhnlichen Seitenanfrage. Da Houston in mehreren Instanzen betrieben wird, reicht eine Sperre innerhalb einer Instanz nicht aus.
Erschwerend kommt hinzu, dass Houston in mehr als einer Instanz betrieben wird. Eine Sperre innerhalb einer Instanz — der naheliegende erste Gedanke — reicht deshalb nicht aus, weil die zweite Instanz von ihr nichts weiß.
Bei der Durchsicht des eigenen Codes im Anschluss an die Umsetzung wurden zwei konkrete Fehlerszenarien gefunden. Beide verletzen Akzeptanzkriterien, die im selben Backlog Item ausdrücklich formuliert worden waren.
Bei der Durchsicht des Codes wurden zwei Fehlerszenarien gefunden, die ausdrücklich formulierte Akzeptanzkriterien verletzen.
\subsection{Szenario A: Datenverlust beim gleichzeitigen Umbenennen}
Das erste Szenario betrifft die Fehlerbehandlung des Umbenennens. Sie entfernt die eigenen Teilkopien, damit das Ziel leer bleibt und der Vorgang wiederholbar ist. Diese Annahme trifft nicht mehr zu, sobald eine zweite Anfrage dieselbe Umbenennung ausführt. Abbildung~\ref{fig:race-condition-a} zeigt den Ablauf.
Das erste Szenario betrifft die Fehlerbehandlung des Umbenennens. Abbildung~\ref{fig:race-condition-a} zeigt den Ablauf.
\begin{figure}[H]
\centering
@@ -20,43 +18,37 @@ Das erste Szenario betrifft die Fehlerbehandlung des Umbenennens. Sie entfernt d
\label{fig:race-condition-a}
\end{figure}
Beide Anfragen halten den Zielnamen für frei, weil beide prüfen, bevor eine von ihnen schreibt. Anfrage~A kopiert alle Objekte und löscht anschließend die Quelle. Anfrage~B, die langsamer ist, hat zu diesem Zeitpunkt erst einen Teil kopiert und findet beim nächsten Objekt die Quelle nicht mehr vor. Ihre Fehlerbehandlung greift und entfernt die von ihr erzeugten Kopien — das sind aber genau dieselben Objekte, die A soeben erfolgreich geschrieben hat. Da die Quelle bereits gelöscht ist, existiert von ihnen keine weitere Kopie.
Das Ergebnis ist endgültiger Datenverlust. Zusätzlich liefert B anschließend ein Präfix zurück, unter dem nichts mehr liegt. Die Fehlerbehandlung, die Datenverlust verhindern sollte, verursacht ihn.
Beide Anfragen halten den Zielnamen für frei, weil beide prüfen, bevor eine schreibt. Anfrage~A kopiert alle Objekte und löscht die Quelle. Anfrage~B findet beim nächsten Objekt die Quelle nicht mehr vor. Ihre Fehlerbehandlung entfernt die eigenen Teilkopien — dieselben Objekte, die A soeben geschrieben hat. Da die Quelle gelöscht ist, existiert keine Kopie mehr. Das Ergebnis ist Datenverlust.
\subsection{Szenario B: Vermischung zweier Mandanten}
Das zweite Szenario betrifft das Anlegen. Der Marker wird ohne Bedingung geschrieben. Haben zwei Organisationen denselben Firmennamen und greifen beide erstmals gleichzeitig zu, sehen beide den Zielnamen als frei an, und beide legen den Marker an. Der zuletzt geschriebene gewinnt.
Der Marker wird ohne Bedingung geschrieben. Haben zwei Organisationen denselben Firmennamen und greifen erstmals gleichzeitig zu, legen beide den Marker an; der zuletzt geschriebene gewinnt. Beide arbeiten anschließend im selben Ordner, obwohl das Metadatum nur einer gehört. Der Folgeaufruf korrigiert sich zwar selbst, doch zwischenzeitlich Abgelegtes verbleibt im fremden Ordner.
Beide Anfragen arbeiten anschließend in demselben Ordner weiter, obwohl das Metadatum nur einer von ihnen gehört. Bis zum nächsten Aufruf sieht die unterlegene Organisation Dokumente in einem Ordner, der laut Metadatum der anderen zugeordnet ist. Der Folgeaufruf korrigiert sich zwar selbst — die unterlegene Organisation erkennt dann den fremden Marker und erhält den um die Organisations-ID ergänzten Namen —, doch was zwischenzeitlich abgelegt wurde, verbleibt im fremden Ordner und ist dort für den falschen Kunden sichtbar.
Von den beiden Szenarien ist dieses das schwerwiegendere: Datenverlust ist ein Betriebsproblem, die Offenlegung von Kundendokumenten gegenüber einem anderen Kunden ein Vertraulichkeitsproblem.
Von den beiden Szenarien ist dieses das schwerwiegendere: Datenverlust ist ein Betriebsproblem, die Offenlegung von Kundendokumenten ein Vertraulichkeitsproblem.
\subsection{Abgegrenzte Fälle}
Zur Eingrenzung wurde geprüft, welche nebenläufigen Abläufe \emph{nicht} betroffen sind. Zwei Anfragen derselben Organisation, die denselben Ordner anlegen, erzeugen identische Schlüssel mit identischen Metadaten und sind harmlos. Zwei Anfragen, die dieselbe Umbenennung vollständig ausführen, erzeugen inhaltsgleiche Kopien; ein Löschen bereits gelöschter Objekte ist unproblematisch. Auch der Fall, dass eine Anfrage liest, während eine andere verschiebt, ist abgedeckt: Da der Marker zuletzt kopiert wird und zusätzlich geprüft wird, ob am Ziel bereits Objekte liegen, wird ein halb gefülltes Ziel nicht als gültiger Kundenordner erkannt.
Diese Abgrenzung ist für die Bewertung wesentlich. Sie zeigt, dass die getroffenen Vorkehrungen wirken und das Problem auf den Fall zweier \emph{unterschiedlich weit fortgeschrittener} verändernder Vorgänge beschränkt ist.
Zur Eingrenzung wurde geprüft, welche nebenläufigen Abläufe \emph{nicht} betroffen sind. Zwei Anfragen derselben Organisation erzeugen identische Schlüssel und sind harmlos. Lesen während einer Verschiebung ist abgedeckt: Da der Marker zuletzt kopiert wird, wird ein halb gefülltes Ziel nicht als Kundenordner erkannt. Das Problem beschränkt sich auf zwei \emph{unterschiedlich weit fortgeschrittene} verändernde Vorgänge.
\subsection{Erwogene Gegenmaßnahmen}
Vier Ansätze wurden formuliert:
\begin{enumerate}
\item \textbf{Absicherung der Fehlerbehandlung.} Vor dem Entfernen der Teilkopien wird geprüft, ob der Marker der Quelle noch existiert. Fehlt er, hat ein anderer Vorgang die Umbenennung bereits abgeschlossen, und es darf nichts gelöscht werden. Das beseitigt Szenario~A weitgehend und verschiebt den verbleibenden Fehler in die ungefährliche Richtung: Es bleiben überzählige Objekte zurück statt Daten verloren zu gehen.
\item \textbf{Bedingtes Schreiben.} Wird der Marker nur unter der Bedingung geschrieben, dass er noch nicht existiert, ist das Beanspruchen eines Namens unteilbar und Szenario~B ausgeschlossen \autocite{aws-conditional-writes}. Voraussetzung ist, dass der eingesetzte Speicher diese vergleichsweise junge Erweiterung unterstützt — was angesichts der Erfahrungen mit anderen Funktionen ausdrücklich nachzuweisen wäre.
\item \textbf{Absicherung der Fehlerbehandlung.} Vor dem Entfernen der Teilkopien wird geprüft, ob der Marker der Quelle noch existiert. Fehlt er, hat ein anderer Vorgang die Umbenennung abgeschlossen, und es darf nichts gelöscht werden. Das beseitigt Szenario~A weitgehend: Es bleiben überzählige Objekte zurück statt Daten verloren zu gehen.
\item \textbf{Bedingtes Schreiben.} Wird der Marker nur unter der Bedingung geschrieben, dass er noch nicht existiert, ist das Beanspruchen eines Namens unteilbar und Szenario~B ausgeschlossen \autocite{aws-conditional-writes}. Voraussetzung ist, dass der Speicher diese vergleichsweise junge Erweiterung unterstützt — was ausdrücklich nachzuweisen wäre.
\item \textbf{Instanzübergreifende Sperre.} Eine über die Datenbank realisierte Sperre je Organisation würde den verändernden Teil sauber serialisieren, kostet aber einen zusätzlichen Zugriff je Anfrage und erfordert ein Konzept für den Fall, dass eine Sperre nicht freigegeben wird.
\item \textbf{Verlagerung aus dem Anfragepfad.} Der verändernde Teil wird gar nicht mehr im Rahmen einer Benutzeranfrage ausgeführt. Bestandsordner werden einmalig kontrolliert migriert, und die Anwendung protokolliert lediglich, wenn ein Ordner vom Sollzustand abweicht. Damit verschwindet die Ursache vollständig statt abgesichert zu werden.
\item \textbf{Verlagerung aus dem Anfragepfad.} Bestandsordner werden einmalig kontrolliert migriert, und die Anwendung protokolliert lediglich, wenn ein Ordner vom Sollzustand abweicht. Damit verschwindet die Ursache vollständig statt abgesichert zu werden.
\end{enumerate}
Der erste Ansatz sollte in jedem Fall umgesetzt werden, unabhängig davon, wie die eigentliche Serialisierung gelöst wird. Der vierte ist insofern bemerkenswert, als er keine technische, sondern eine organisatorische Lösung darstellt — er beseitigt das Problem, indem er den verändernden Vorgang aus dem nebenläufigen Kontext herausnimmt.
Der erste Ansatz sollte unabhängig von der Serialisierungslösung umgesetzt werden. Der vierte ist bemerkenswert, da er eine organisatorische statt technische Lösung darstellt.
\subsection{Umgang mit dem Befund}
Der Befund wurde als eigenes Backlog Item erfasst, mit erhöhter Priorität versehen und ausdrücklich vom laufenden Pull Request abgegrenzt. Diese Abgrenzung erfolgte nach Absprache im Team und wurde am Item dokumentiert. Das Team versah es mit einem Zeitrahmen von vier bis sechs Stunden.
Der Befund wurde als eigenes Backlog Item mit erhöhter Priorität erfasst und vom laufenden Pull Request abgegrenzt. Das Team versah es mit einem Zeitrahmen von vier bis sechs Stunden.
Die Alternative wäre gewesen, den Pull Request so lange offenzuhalten, bis auch die Nebenläufigkeit gelöst ist. Dagegen sprach, dass die Wahl der Gegenmaßnahme selbst eine offene Frage ist: Ob bedingtes Schreiben zur Verfügung steht, ließ sich angesichts der Erfahrungen mit anderen Speicherfunktionen nicht ohne Rückfrage beim Betreiber beantworten, und deren Bearbeitungsdauer war zuvor mit mehreren Wochen bemessen worden. Der Lookup selbst — die eigentliche Verbesserung — wäre dadurch blockiert worden, obwohl er den Zustand gegenüber vorher bereits deutlich verbessert.
Den Pull Request offenzuhalten, bis die Nebenläufigkeit gelöst ist, hätte den Lookup blockiert: Ob bedingtes Schreiben verfügbar ist, ließ sich ohne Rückfrage beim Betreiber nicht beantworten, und deren Bearbeitungsdauer war zuvor mit mehreren Wochen bemessen worden. Der Lookup verbessert den Zustand gegenüber vorher bereits deutlich.
Hinzu kommt, dass der verändernde Pfad ausschließlich beim ersten Zugriff einer Organisation durchlaufen wird. Nach der einmaligen Selbstheilung ist er für diesen Kunden dauerhaft irrelevant. Das Risikofenster ist damit eng begrenzt — es besteht pro Kunde genau einmal.
Hinzu kommt, dass der verändernde Pfad nur beim ersten Zugriff einer Organisation durchlaufen wird. Das Risikofenster besteht pro Kunde genau einmal.
Die Akzeptanzkriterien des Folgeitems halten fest, dass eine reine Sperre innerhalb einer Instanz ausdrücklich nicht als Lösung akzeptiert wird und dass beide Szenarien durch Unit-Tests nachzubilden sind, die ohne die Korrektur fehlschlagen.
Die Akzeptanzkriterien des Folgeitems halten fest, dass eine reine Sperre innerhalb einer Instanz nicht als Lösung akzeptiert wird und dass beide Szenarien durch Unit-Tests nachzubilden sind.
+8 -14
View File
@@ -3,30 +3,24 @@
\subsection{Zugriff über das AWS SDK}
Der Zugriff auf den Speicher erfolgt über das AWS SDK für .NET. Obwohl der Speicher nicht bei Amazon betrieben wird, ist dies der naheliegende Weg: StorageGRID implementiert die S3-Schnittstelle, und das SDK lässt sich über die Angabe einer abweichenden Dienstadresse auf einen beliebigen kompatiblen Endpunkt richten. Eine eigene Implementierung der Protokolldetails — insbesondere der Signaturberechnung — wäre aufwendig und fehleranfällig gewesen.
Der Zugriff auf den Speicher erfolgt über das AWS SDK für .NET. Obwohl der Speicher nicht bei Amazon betrieben wird, ist dies der naheliegende Weg: StorageGRID implementiert die S3-Schnittstelle, und das SDK lässt sich über eine abweichende Dienstadresse auf einen beliebigen kompatiblen Endpunkt richten. Eine eigene Implementierung — insbesondere der Signaturberechnung — wäre aufwendig und fehleranfällig gewesen.
Der \texttt{S3DocumentsClient} kapselt die verwendeten Operationen. Er ist bewusst schmal gehalten und bietet nur die tatsächlich benötigten Zugriffe an: das Auflisten von Objekten unterhalb eines Präfixes, das Abrufen der Metadaten eines einzelnen Objekts, das Erzeugen zeitlich begrenzter Zugriffs-URLs, das Lesen eines Objektinhalts sowie — für die Ordnerverwaltung — das Anlegen, Kopieren und Löschen von Objekten.
Diese Kapselung erfüllt zwei Zwecke. Zum einen hält sie die SDK-spezifischen Anfrage- und Antworttypen aus der fachlichen Schicht heraus. Zum anderen ist sie die Voraussetzung dafür, den Speicherzugriff in Tests durch ein Mock zu ersetzen (siehe Abschnitt~\ref{sec:unit-tests}).
Der \texttt{S3DocumentsClient} kapselt die verwendeten Operationen: Auflisten, Metadatenabruf, Erzeugen zeitlich begrenzter Zugriffs-URLs, Lesen von Objektinhalten sowie Anlegen, Kopieren und Löschen von Objekten. Diese Kapselung hält die SDK-spezifischen Typen aus der fachlichen Schicht und ermöglicht in Tests ein Mock (siehe Abschnitt~\ref{sec:unit-tests}).
\subsection{Konfiguration mehrerer Speicher}
Eine Besonderheit ergab sich daraus, dass Houston bereits vor diesem Projekt einen S3-Speicher verwendete — für die Anbindung des Dokumentationssystems. Mit dem Dokumentenbereich kam ein zweiter, davon unabhängiger Speicher hinzu, mit eigenem Bucket und eigenen Zugangsdaten.
Houston verwendete bereits vor diesem Projekt einen S3-Speicher für das Dokumentationssystem. Mit dem Dokumentenbereich kam ein zweiter, davon unabhängiger Speicher hinzu.
In der ersten Fassung wurden die Einstellungen des neuen Speichers als eigenständiger Satz von Konfigurationswerten geführt. Im Review wurde angeregt, stattdessen eine gemeinsame Struktur für S3-Einstellungen zu verwenden und die beiden Verwendungen über benannte Registrierungen im Dienstcontainer auseinanderzuhalten \autocite{dotnet-keyed-di}.
Der Vorteil dieser Lösung liegt in der Erweiterbarkeit: Ein dritter Speicher erfordert lediglich einen weiteren Konfigurationsabschnitt und eine weitere Registrierung, nicht aber eine erneute Verdopplung der Einstellungsklassen. Zudem ist an der Registrierung unmittelbar ablesbar, welcher Programmteil auf welchen Speicher zugreift — bei zwei gleichartig benannten Konfigurationssätzen wäre diese Zuordnung nur aus dem Kontext erkennbar gewesen.
In der ersten Fassung wurden die Einstellungen als eigenständiger Konfigurationssatz geführt. Im Review wurde angeregt, eine gemeinsame Struktur für S3-Einstellungen zu verwenden und die Verwendungen über benannte Registrierungen im Dienstcontainer auseinanderzuhalten \autocite{dotnet-keyed-di}. Ein dritter Speicher erfordert so lediglich einen weiteren Konfigurationsabschnitt und eine Registrierung, nicht aber eine Verdopplung der Einstellungsklassen.
\subsection{Umgebungen und Zugangsdaten}
Für die drei Umgebungen existiert je ein eigener Bucket. Die Trennung erfolgt damit nicht über Präfixe innerhalb eines gemeinsamen Buckets, sondern über getrennte Buckets mit getrennten Zugangsdaten. Ein fehlerhaft konfigurierter Entwicklungsstand kann dadurch nicht auf Produktivdaten zugreifen.
Die Zugangsdaten selbst liegen nicht im Quelltext, sondern werden über die Konfigurationsmechanismen der Anwendung bereitgestellt; hinterlegt sind sie im unternehmensweiten Passwortmanager. Für die lokale Entwicklung wurden die Entwicklungseinstellungen um die entsprechenden Werte ergänzt.
Für die drei Umgebungen existiert je ein eigener Bucket mit getrennten Zugangsdaten, sodass ein fehlerhaft konfigurierter Entwicklungsstand nicht auf Produktivdaten zugreifen kann. Die Zugangsdaten liegen nicht im Quelltext, sondern werden über die Konfigurationsmechanismen der Anwendung bereitgestellt und im unternehmensweiten Passwortmanager hinterlegt.
\subsection{Auflisten von Objekten}
Die zentrale Leseoperation ist das Auflisten von Objekten unterhalb eines Präfixes. Sie wird mit dem Kundenpräfix aufgerufen und liefert sämtliche Objekte des jeweiligen Kunden — über alle Typordner hinweg, da die Anzeige eine flache Liste ist.
Die zentrale Leseoperation listet Objekte unterhalb eines Präfixes auf. Sie wird mit dem Kundenpräfix aufgerufen und liefert sämtliche Objekte über alle Typordner hinweg.
Zwei Eigenschaften dieser Operation prägten die Umsetzung. Erstens liefert sie Ergebnisse blockweise: Überschreitet die Trefferzahl eine bestimmte Größe, wird ein Fortsetzungsmerkmal zurückgegeben, mit dem der nächste Block abgerufen werden kann \autocite{aws-listobjectsv2}. Diese Eigenschaft wurde für die Paginierung genutzt (siehe Abschnitt~\ref{sec:filter-pagination}). Zweitens liefert sie zwar Schlüssel, Größe und Änderungszeitpunkt, aber keine benutzerdefinierten Metadaten. Genau diese Einschränkung ist die Ursache des in Abschnitt~\ref{sec:lookup-research} beschriebenen Lookup-Problems.
Zwei Eigenschaften prägten die Umsetzung. Erstens liefert die Operation Ergebnisse blockweise: Überschreitet die Trefferzahl eine Grenze, wird ein Fortsetzungsmerkmal zurückgegeben \autocite{aws-listobjectsv2}. Diese Eigenschaft wurde für die Paginierung genutzt (siehe Abschnitt~\ref{sec:filter-pagination}). Zweitens liefert sie keine benutzerdefinierten Metadaten — die Ursache des in Abschnitt~\ref{sec:lookup-research} beschriebenen Lookup-Problems.
Beim Auflisten werden zwei Arten von Einträgen herausgefiltert. Zum einen die Platzhalterobjekte, die die Typordner repräsentieren — sie sind technisch Objekte, fachlich aber keine Dokumente. Zum anderen der Marker des Kundenordners selbst. Beide erkennt der Dienst daran, dass ihr Schlüssel auf das Trennzeichen endet.
Beim Auflisten werden Platzhalterobjekte der Typordner und der Marker des Kundenordners herausgefiltert. Beide erkennt der Dienst daran, dass ihr Schlüssel auf das Trennzeichen endet.
+6 -12
View File
@@ -3,26 +3,20 @@
\subsection{Umsetzung}
Die Suche filtert die Dokumentenliste anhand des Dokumentnamens und berücksichtigt dabei Teiltreffer. Ein geleertes Suchfeld stellt die vollständige Liste wieder her. Fachlich beschränkt sie sich bewusst auf den Namen: Im Approval-Termin wurde gefragt, ob auch eine Beschreibung durchsucht werden solle; da Dokumente im Speicher keine Beschreibung tragen, wurde die Suche auf den Namen begrenzt.
Die Suche filtert die Dokumentenliste anhand des Namens und berücksichtigt Teiltreffer. Ein geleertes Suchfeld stellt die vollständige Liste wieder her. Im Approval-Termin wurde gefragt, ob auch eine Beschreibung durchsucht werden solle; da Dokumente keine tragen, wurde die Suche auf den Namen begrenzt.
\subsection{Der Begriff „serverseitig"}
Die Anforderung verlangt eine serverseitige Suche. Dieser Begriff bedarf in diesem Zusammenhang einer Präzisierung, weil er auf zwei verschiedene Ebenen zutreffen kann.
Die Anforderung verlangt eine serverseitige Suche. Gemeint ist, dass die Filterung in der Anwendung stattfindet, nicht im Browser — der Browser erhält die bereits gefilterte Menge. Eine Filterung im Browser würde voraussetzen, dass sämtliche Dokumente zuvor übertragen wurden, was mit der Paginierung unvereinbar wäre.
Gemeint und umgesetzt ist, dass die Filterung in der Anwendung stattfindet und nicht im Browser des Benutzers. Der Browser erhält also bereits die gefilterte Menge. Das ist die für die Anforderung entscheidende Eigenschaft, denn eine Filterung im Browser würde voraussetzen, dass sämtliche Dokumente zuvor übertragen wurden — was mit der Paginierung unvereinbar wäre und bei großen Beständen unnötig Daten überträgt.
Nicht umgesetzt — und mit den verfügbaren Mitteln nicht umsetzbar — ist eine Filterung durch den \emph{Speicher}. Houston listet die Objekte des Kundenordners auf und filtert die zurückgelieferten Namen anschließend selbst. Die Frage, ob sich das nicht direkt über die Schnittstelle lösen lasse, wurde im Review von \emph{Hanna Ebner} gestellt und musste verneint werden.
Nicht umgesetzt ist eine Filterung durch den \emph{Speicher}. Houston listet die Objekte auf und filtert die Namen anschließend selbst. Im Review stellte \emph{Hanna Ebner} die Frage, ob sich das nicht direkt über die Schnittstelle lösen lasse, und musste verneint werden.
\subsection{Warum keine Filterung im Speicher möglich war}
Im weiteren Verlauf des Reviews brachte \emph{Timo Walter} die zuvor gemeinsam betrachtete Abfragefunktion des Speichers ins Spiel. Die Prüfung ergab, dass diese Funktion Objekt\emph{inhalte} filtert und nicht Objektnamen — sie ist dafür gedacht, aus einer strukturierten Datei einzelne Datensätze auszuwählen \autocite{aws-s3-select}. Für eine Namenssuche über mehrere Objekte hinweg ist sie nicht geeignet.
Im weiteren Verlauf brachte \emph{Timo Walter} die Abfragefunktion des Speichers ins Spiel. Die Prüfung ergab, dass diese Objektinhalte filtert, nicht Objektnamen — sie wählt aus strukturierten Dateien einzelne Datensätze aus \autocite{aws-s3-select}.
Als zweite Möglichkeit wurde der Suchdienst des Speicherherstellers erwogen, der Objektmetadaten in einen durchsuchbaren Index spiegelt \autocite{storagegrid-search-integration}. Zum Zeitpunkt des Reviews stand dessen Verfügbarkeit noch nicht fest; die Anfrage über den Betreiber lief bereits (siehe Abschnitt~\ref{sec:infrastructure}). Die spätere Antwort fiel negativ aus.
Auf Anregung des Reviewers wurde dieser Umstand nicht nur im Gesprächsverlauf festgehalten, sondern ausdrücklich in das Recherche-Item aufgenommen. Damit war die Suche nicht länger eine Funktion mit einer unausgesprochenen Schwäche, sondern eine Funktion mit einer dokumentierten und einem Folgeitem zugeordneten Einschränkung.
Als zweite Möglichkeit wurde der Suchdienst des Speicherherstellers erwogen \autocite{storagegrid-search-integration}. Dessen Verfügbarkeit stand noch nicht fest; die Anfrage lief bereits (siehe Abschnitt~\ref{sec:infrastructure}). Die spätere Antwort fiel negativ aus. Auf Anregung des Reviewers wurde dieser Umstand in das Recherche-Item aufgenommen.
\subsection{Bewertung}
Die praktische Auswirkung ist derzeit gering. Die Zahl der Dokumente je Kunde bewegt sich in einer Größenordnung, in der das Auflisten und anschließende Filtern nicht spürbar ins Gewicht fällt — anders als beim Kundenordner-Lookup, der über \emph{alle} Kunden iterierte und daher mit der Kundenzahl skalierte. Die Suche skaliert dagegen nur mit der Dokumentenzahl eines einzelnen Kunden.
Damit war die Priorisierung klar: Der Lookup wurde noch im Projektzeitraum gelöst, die Suchoptimierung blieb ein Thema für den Ausblick.
Die praktische Auswirkung ist derzeit gering. Die Dokumentenzahl je Kunde ist überschaubar; die Suche skaliert nur mit der Dokumentenzahl eines einzelnen Kunden, nicht mit der Kundenzahl. Der Lookup wurde daher im Projektzeitraum gelöst, die Suchoptimierung blieb für den Ausblick.
+4 -6
View File
@@ -3,16 +3,14 @@
\subsection{Eigene Symbole statt Symbolschrift}
Houston verwendet für Symbole eine gängige Symbolbibliothek. Für die sieben Dokumententypen enthielt diese jedoch keine passenden Motive — Begriffe wie „SLA-Report Ticketbearbeitung" oder „Security Assessment" lassen sich mit allgemeinen Symbolen nicht sinnvoll unterscheiden. Es wurden daher eigene Vektorgrafiken erstellt.
Houston verwendet für Symbole eine gängige Symbolbibliothek. Für die sieben Dokumententypen enthielt diese keine passenden Motive — Begriffe wie „SLA-Report Ticketbearbeitung" lassen sich mit allgemeinen Symbolen nicht unterscheiden. Es wurden daher eigene Vektorgrafiken erstellt.
Damit stellte sich die Frage, wie diese in die bestehende Oberfläche eingebunden werden. Eine Einbindung als gewöhnliche Grafik hätte bedeutet, dass die Symbole eine feste Farbe tragen. Houston unterstützt jedoch ein helles und ein dunkles Erscheinungsbild, und die umgebenden Symbole der Bibliothek passen sich der Textfarbe an. Fest eingefärbte Symbole wären im dunklen Erscheinungsbild kaum sichtbar gewesen.
Eine Einbindung als gewöhnliche Grafik hätte eine feste Farbe bedeutet. Houston unterstützt jedoch ein helles und ein dunkles Erscheinungsbild, und fest eingefärbte Symbole wären im dunklen kaum sichtbar gewesen.
\subsection{Einfärbung über Masken}
Gelöst wurde dies, indem die Vektorgrafiken nicht als Bild eingebunden, sondern als Maske über einer Hintergrundfläche verwendet werden. Die Fläche übernimmt dabei die aktuelle Textfarbe, sodass sich das Symbol automatisch an das Erscheinungsbild anpasst — genau wie die Symbole der Bibliothek.
Der Ansatz hat den zusätzlichen Vorteil, dass sich die eigenen Symbole über dieselben Gestaltungsklassen ansprechen lassen wie die vorhandenen. Für die Verwendung in der Seite ist damit nicht erkennbar, ob ein Symbol aus der Bibliothek stammt oder eigens erstellt wurde. Als Nebenbedingung ergab sich, dass alle Grafiken dieselben Abmessungen haben müssen, damit sie im Textfluss gleich ausgerichtet erscheinen. Im Review fiel auf, dass eine der Grafiken von den übrigen abwich; die Maße wurden angeglichen.
Gelöst wurde dies, indem die Vektorgrafiken als Maske über einer Hintergrundfläche dienen. Die Fläche übernimmt die aktuelle Textfarbe, sodass sich das Symbol automatisch anpasst. Die eigenen Symbole lassen sich über dieselben Gestaltungsklassen ansprechen wie die vorhandenen. Alle Grafiken müssen dieselben Abmessungen haben; im Review fiel auf, dass eine abwich, und die Maße wurden angeglichen.
\subsection{Standardsymbol}
Dokumente ohne erkennbaren Typ — solche, die unmittelbar im Kundenordner liegen oder in einem unbekannten Unterordner — erhalten ein neutrales Standardsymbol. Damit ist sichergestellt, dass jede Zeile ein Symbol trägt und die Liste optisch gleichmäßig bleibt. Zugleich ist an der Darstellung erkennbar, dass für dieses Dokument keine Typzuordnung vorliegt, ohne dass dies wie ein Fehler wirkt.
Dokumente ohne erkennbaren Typ erhalten ein neutrales Standardsymbol. Damit trägt jede Zeile ein Symbol, und an der Darstellung ist erkennbar, dass keine Typzuordnung vorliegt, ohne dass dies wie ein Fehler wirkt.
+7 -17
View File
@@ -3,36 +3,26 @@
\subsection{Anwendungsfall}
Nicht alle Unterlagen, die für einen Kunden relevant sind, lassen sich sinnvoll in den Speicher kopieren. Ein fortlaufend gepflegtes Dokument in einem Dokumentenverwaltungssystem oder ein Kanal in einem Kollaborationswerkzeug soll verlinkt und nicht dupliziert werden — eine Kopie wäre sofort veraltet.
Nicht alle Unterlagen lassen sich sinnvoll in den Speicher kopieren. Ein fortlaufend gepflegtes Dokument oder ein Kanal in einem Kollaborationswerkzeug soll verlinkt werden — eine Kopie wäre sofort veraltet. Dafür werden \texttt{.url}-Dateien unterstützt: kleine Textdateien im INI-Format mit Zieladresse und optionalem Symbol. In der Liste erscheinen sie wie gewöhnliche Dokumente, führen beim Anklicken jedoch auf die hinterlegte Adresse.
Für diesen Zweck werden \texttt{.url}-Dateien unterstützt. Dabei handelt es sich um ein etabliertes Format für Verknüpfungen: eine kleine Textdatei, die im INI-Format die Zieladresse und optional ein Symbol enthält. Legt ein Kundenbetreuer eine solche Datei im Speicher ab, erscheint sie in der Dokumentenliste wie ein gewöhnliches Dokument, führt beim Anklicken jedoch auf die hinterlegte Adresse.
Der Anwendungsfall war zunächst missverständlich formuliert und wurde in der Feature-Analyse geklärt: Gemeint ist das Einbetten \emph{externer} Ressourcen in den Dokumentenbereich, nicht das Teilen von Houston-Dokumenten nach außen (siehe Abschnitt~\ref{sec:feature-analysis}).
Der Anwendungsfall wurde in der Feature-Analyse geklärt: Gemeint ist das Einbetten \emph{externer} Ressourcen, nicht das Teilen von Houston-Dokumenten nach außen (siehe Abschnitt~\ref{sec:feature-analysis}).
\subsection{Auswertung des Dateiformats}
Für das Auswerten des INI-Formats wurde bewusst keine externe Bibliothek eingebunden, sondern eine kleine eigene Auswertung geschrieben. Der Grund ist das Verhältnis von Aufwand zu Nutzen: Benötigt wird ein einziger Wert aus einem einzigen Abschnitt. Eine allgemeine Bibliothek für ein Format, von dem ein sehr kleiner Ausschnitt gebraucht wird, brächte eine dauerhaft zu pflegende Abhängigkeit mit sich — samt Aktualisierungen, Lizenzprüfung und einer weiteren Position in der Abhängigkeitsliste — für Funktionalität, die in wenigen Zeilen selbst geschrieben ist.
Hinzu kommt, dass eine allgemeine Bibliothek in diesem Fall gar nicht das gewünschte Verhalten böte. Sie würde bei fehlerhaften Dateien vermutlich einen Fehler melden, während hier ein stiller Rückfall erforderlich ist (siehe unten). Die eigene Auswertung ist auf genau dieses Verhalten hin geschrieben.
Für das INI-Format wurde bewusst keine externe Bibliothek eingebunden. Benötigt wird ein einziger Wert aus einem Abschnitt. Eine Bibliothek brächte eine dauerhaft zu pflegende Abhängigkeit für Funktionalität, die in wenigen Zeilen selbst geschrieben ist. Zudem würde sie bei fehlerhaften Dateien vermutlich einen Fehler melden, während hier ein stiller Rückfall erforderlich ist.
\subsection{Rückfall auf normales Verhalten}
Kann in einer \texttt{.url}-Datei keine gültige Adresse erkannt werden, wird sie wie eine gewöhnliche Datei behandelt und zum Herunterladen angeboten. Dieses Verhalten war ausdrücklich gefordert.
Die Überlegung dahinter ist, dass eine fehlerhafte Verknüpfungsdatei kein Grund sein darf, das Dokument unzugänglich zu machen. Der Kunde erhält im schlechtesten Fall eine kleine Textdatei statt einer Weiterleitung — ein nachvollziehbares Ergebnis, aus dem sich zudem ablesen lässt, was schiefgegangen ist. Die Alternative, einen Fehler anzuzeigen oder den Eintrag auszublenden, wäre für den Kunden weniger hilfreich.
Kann keine gültige Adresse erkannt werden, wird die Datei wie eine gewöhnliche Datei zum Herunterladen angeboten. Eine fehlerhafte Verknüpfungsdatei darf das Dokument nicht unzugänglich machen.
\subsection{Anzeigename}
Der Dateiname einer Verknüpfung endet auf \texttt{.url}. Diese Endung ist für den Kunden bedeutungslos und würde in der Liste lediglich stören. Sie wird deshalb für die Anzeige entfernt.
Im Review wies \emph{Robin Noack} darauf hin, dass die dafür verwendete Zeichenzahl als unmittelbare Zahl im Code stand, und schlug vor, stattdessen die Länge der Endung selbst zu verwenden. Der Hinweis ist auf den ersten Blick eine Kleinigkeit, trifft aber einen realen Fehlerfall: Zahl und Endung stehen an verschiedenen Stellen, und wird die eine geändert, bleibt die andere unbemerkt zurück.
Die Endung \texttt{.url} wird für die Anzeige entfernt. Im Review wies \emph{Robin Noack} darauf hin, dass die dafür verwendete Zeichenzahl als unmittelbare Zahl im Code stand; stattdessen sollte die Länge der Endung selbst verwendet werden, da Zahl und Endung an verschiedenen Stellen stehen und bei einer Änderung auseinanderlaufen können.
\subsection{Symbole für Verknüpfungen}
Das INI-Format sieht die Angabe eines Symbols vor. Da benutzerdefinierte Symbole aus den in Abschnitt~\ref{sec:pdf-preview} genannten Gründen nicht ausgeliefert werden, wurde ein anderer Weg gewählt: Es stehen benannte Voreinstellungen zur Verfügung, die auf Symbole der in Houston vorhandenen Bibliothek verweisen. Für die häufigsten Ziele — die Dateiablage, das Dokumentenverwaltungssystem und das Kollaborationswerkzeug des Unternehmens — wurden eigene Symbole ergänzt.
Damit erhält ein Kundenbetreuer die gewünschte visuelle Unterscheidung, ohne dass Dateiinhalt an einer nicht authentifizierten Stelle verarbeitet werden muss. Da diese Voreinstellungen nur nützen, wenn sie bekannt sind, wurden die verfügbaren Namen im internen Wiki dokumentiert und im Pull Request darauf verwiesen.
Da benutzerdefinierte Symbole aus den in Abschnitt~\ref{sec:pdf-preview} genannten Gründen nicht ausgeliefert werden, stehen stattdessen benannte Voreinstellungen zur Verfügung, die auf Symbole der in Houston vorhandenen Bibliothek verweisen. Für die häufigsten Ziele — Dateiablage, Dokumentenverwaltungssystem und Kollaborationswerkzeug — wurden eigene Symbole ergänzt. Die verfügbaren Namen wurden im internen Wiki dokumentiert und im Pull Request darauf verwiesen.
\subsection{Ausschluss vom ZIP-Download}
Verknüpfungsdateien erhalten keine Auswahlbox und können damit nicht Teil eines Archivs werden. Der Grund ist, dass ein Archiv sonst eine Datei enthielte, die beim Öffnen nicht das erwartete Dokument liefert, sondern eine Verknüpfung, die möglicherweise nur innerhalb des Unternehmensnetzes auflösbar ist. Da eine Verknüpfung fachlich kein Dokument ist, sondern ein Verweis, wäre ihre Aufnahme in ein Dokumentenarchiv irreführend.
Verknüpfungsdateien erhalten keine Auswahlbox und können nicht Teil eines Archivs werden. Ein Archiv enthielte sonst eine Datei, die beim Öffnen eine möglicherweise nur intern auflösbare Verknüpfung liefert. Da eine Verknüpfung fachlich kein Dokument ist, wäre ihre Aufnahme irreführend.