From f857a244dd5ca1f3fcc86880f0396f81b0790b77 Mon Sep 17 00:00:00 2001 From: 0qln <0qln@proton.me> Date: Tue, 25 Aug 2026 20:33:15 +0200 Subject: [PATCH] =?UTF-8?q?chap=205:=20Umsetzung=20=E2=80=94=2011=20Abschn?= =?UTF-8?q?itte=20+=20module-components/zip-stream/race-condition=20diagra?= =?UTF-8?q?ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapters/execution/architecture.tex | 47 ++++++++++++--- chapters/execution/document-explorer.tex | 31 +++++++++- chapters/execution/downloads.tex | 46 +++++++++++---- chapters/execution/filter-pagination.tex | 32 +++++++++- chapters/execution/folder-management.tex | 42 ++++++++++++-- chapters/execution/pdf-preview.tex | 34 +++++++++-- chapters/execution/race-conditions.tex | 71 +++++++++++++++++++---- chapters/execution/s3-client.tex | 31 +++++++++- chapters/execution/search.tex | 30 ++++++++-- chapters/execution/type-icons.tex | 19 +++++- chapters/execution/url-files.tex | 40 +++++++++++-- figures/diagrams/module-components.mmd | 68 +++++++++++++--------- figures/diagrams/module-components.pdf | Bin 0 -> 34006 bytes figures/diagrams/race-condition-a.mmd | 30 ++++++---- figures/diagrams/race-condition-a.pdf | Bin 0 -> 28607 bytes figures/diagrams/zip-stream.mmd | 26 +++++---- figures/diagrams/zip-stream.pdf | Bin 0 -> 40708 bytes 17 files changed, 437 insertions(+), 110 deletions(-) create mode 100644 figures/diagrams/module-components.pdf create mode 100644 figures/diagrams/race-condition-a.pdf create mode 100644 figures/diagrams/zip-stream.pdf diff --git a/chapters/execution/architecture.tex b/chapters/execution/architecture.tex index a673dad..2f49369 100644 --- a/chapters/execution/architecture.tex +++ b/chapters/execution/architecture.tex @@ -1,12 +1,41 @@ -\section{Architektur des Documents-Moduls} +\section{Architektur des Dokumentenmoduls} \label{sec:architecture} -% TODO: Razor Page DocumentsPage.cshtml → DocumentsService → S3DocumentsClient -% Klassendiagramm module-components.pdf +\subsection{Schichtung} -% \begin{figure}[H] -% \centering -% \includegraphics[width=0.9\textwidth]{figures/diagrams/module-components.pdf} -% \caption{Komponentenstruktur des Documents-Moduls} -% \label{fig:module-components} -% \end{figure} +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. + +\begin{figure}[H] + \centering + \includegraphics[width=0.95\textwidth]{figures/diagrams/module-components.pdf} + \caption{Komponenten des Dokumentenmoduls} + \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. + +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. + +\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. + +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. + +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. + +\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. + +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. + +\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. + +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 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. diff --git a/chapters/execution/document-explorer.tex b/chapters/execution/document-explorer.tex index b1ecd74..abb2acf 100644 --- a/chapters/execution/document-explorer.tex +++ b/chapters/execution/document-explorer.tex @@ -1,5 +1,32 @@ \section{Document Explorer} \label{sec:document-explorer} -% TODO: PBI 9295 — /documents Seite, ListObjectsV2, Empty State, Fehlerbehandlung -% Abbildung~\ref{fig:documents-page-model}, \ref{fig:document-view-model}, \ref{fig:documents-table-body} +\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. + +\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. + +\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 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. + +\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. + +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. + +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. + +\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. + +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}. diff --git a/chapters/execution/downloads.tex b/chapters/execution/downloads.tex index 509c561..ddccaf9 100644 --- a/chapters/execution/downloads.tex +++ b/chapters/execution/downloads.tex @@ -1,13 +1,39 @@ -\section{Dokumenten-Downloads} +\section{Downloads} \label{sec:downloads} -% TODO: PBI 9299 (Einzeldownload), PBI 9296 (ZIP-Download) -% Streaming: S3 GetObject → ZipArchive → HTTP Response Stream -% Abbildung~\ref{fig:zip-download-action} +\subsection{Einzeldownload über zeitlich begrenzte Zugriffs-URLs} -% \begin{figure}[H] -% \centering -% \includegraphics[width=0.9\textwidth]{figures/diagrams/zip-stream.pdf} -% \caption{ZIP-Download-Ablauf} -% \label{fig:zip-stream} -% \end{figure} +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}. + +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 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. + +\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. + +Abbildung~\ref{fig:zip-stream} zeigt den Ablauf. + +\begin{figure}[H] + \centering + \includegraphics[width=0.95\textwidth]{figures/diagrams/zip-stream.pdf} + \caption{Ablauf des ZIP-Downloads} + \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. + +\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. diff --git a/chapters/execution/filter-pagination.tex b/chapters/execution/filter-pagination.tex index e4f2550..44c5fb4 100644 --- a/chapters/execution/filter-pagination.tex +++ b/chapters/execution/filter-pagination.tex @@ -1,6 +1,32 @@ \section{Typfilter und Paginierung} \label{sec:filter-pagination} -% TODO: PBI 9749 (Typfilter), PBI 9731 (Pagination) -% ContinuationToken statt Offset-Pagination -% Abbildung~\ref{fig:pagination-continuation-token} +\subsection{Filterung nach Dokumententyp} + +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 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. + +\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. + +\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. + +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. + +\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. diff --git a/chapters/execution/folder-management.tex b/chapters/execution/folder-management.tex index 213c6fb..a60159a 100644 --- a/chapters/execution/folder-management.tex +++ b/chapters/execution/folder-management.tex @@ -1,8 +1,38 @@ -\section{Ordnerstruktur und Kundenordner-Lookup} +\section{Ordnerverwaltung und Kundenordner-Lookup} \label{sec:folder-management} -% TODO: PBI 9560 (Ordneranlage beim ersten Seitenaufruf), PBI 10018 (O(1)-Lookup) -% OrgSlug: deterministische, S3-konforme Normalisierung des Efecte-Namens (Umlaute bleiben erhalten) -% IBM-Naming-Constraints: \cite{ibm-s3-naming} -% ResolveCustomerPrefixAsync, Self-Healing-Rename -% Abbildung~\ref{fig:org-slug}, \ref{fig:resolve-customer-prefix}, \ref{fig:rename-customer-folder} +\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. + +\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. + +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. + +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. + +\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. + +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. + +\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. + +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, 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. + +\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. diff --git a/chapters/execution/pdf-preview.tex b/chapters/execution/pdf-preview.tex index 4b38385..1368212 100644 --- a/chapters/execution/pdf-preview.tex +++ b/chapters/execution/pdf-preview.tex @@ -1,6 +1,32 @@ -\section{PDF-Modal-Vorschau und Share-Link-Previews} +\section{PDF-Vorschau und Freigabelinks} \label{sec:pdf-preview} -% TODO: PBI 9300 (PDF-Modal), PBI 9294 (Share-Link-Previews) -% Pre-Signed URLs, zeitlich begrenzte Gültigkeit -% Abbildung~\ref{fig:presigned-url} +\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. + +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. + +\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. + +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. + +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. + +\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. + +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. + +\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. + +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. diff --git a/chapters/execution/race-conditions.tex b/chapters/execution/race-conditions.tex index 071c8fd..ed67829 100644 --- a/chapters/execution/race-conditions.tex +++ b/chapters/execution/race-conditions.tex @@ -1,15 +1,62 @@ -\section{Bekannte Grenze: Race Conditions im Lookup} +\section{Erkannte Grenze: Nebenläufigkeit im Lookup} \label{sec:race-conditions} -% TODO: PBI 10070 — Houston läuft in mehreren Instanzen, In-Memory-Lock reicht nicht -% Szenario A: Datenverlust beim parallelen Rename (Rollback löscht Dokumente des Gewinners) -% Szenario B: Mandantenvermischung beim gleichzeitigen Anlegen gleicher Efecte-Namen -% Lösungsansätze: Rollback-Absicherung, Conditional Writes, verteiltes Lock, prozessuale Migration -% Timebox: 4–6 Stunden (Stephan Janßen) +\subsection{Ausgangslage} -% \begin{figure}[H] -% \centering -% \includegraphics[width=0.9\textwidth]{figures/diagrams/race-condition-a.pdf} -% \caption{Race Condition Szenario A: Datenverlust beim parallelen Rename} -% \label{fig:race-condition-a} -% \end{figure} +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. + +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. + +\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. + +\begin{figure}[H] + \centering + \includegraphics[width=\textwidth]{figures/diagrams/race-condition-a.pdf} + \caption{Datenverlust bei gleichzeitigem Umbenennen} + \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. + +\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. + +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. + +\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. + +\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{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. +\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. + +\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. + +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. + +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. + +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. diff --git a/chapters/execution/s3-client.tex b/chapters/execution/s3-client.tex index ff7f07e..74ad5b6 100644 --- a/chapters/execution/s3-client.tex +++ b/chapters/execution/s3-client.tex @@ -1,5 +1,32 @@ \section{Anbindung des S3-Speichers} \label{sec:s3-client} -% TODO: AWS SDK für .NET, IAmazonS3, keyed services in Program.cs, S3Settings (DEV/TEST/PROD Buckets) -% Abbildung~\ref{fig:s3-settings-registration}, \ref{fig:s3-documents-client} +\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 \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}). + +\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. + +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. + +\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. + +\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. + +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. + +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. diff --git a/chapters/execution/search.tex b/chapters/execution/search.tex index 31c6927..d0ab4fd 100644 --- a/chapters/execution/search.tex +++ b/chapters/execution/search.tex @@ -1,6 +1,28 @@ -\section{Serverseitige Suche} +\section{Suche} \label{sec:search} -% TODO: PBI 9298 — Suchleiste, titelbasiert, Teiltreffer, serverseitiges Filtern -% S3-API bietet keine direkte Filterung → Houston-seitiger Filter -% Abbildung~\ref{fig:document-search} +\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. + +\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. + +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. + +\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. + +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. + +\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. diff --git a/chapters/execution/type-icons.tex b/chapters/execution/type-icons.tex index a0a17e2..7830e67 100644 --- a/chapters/execution/type-icons.tex +++ b/chapters/execution/type-icons.tex @@ -1,5 +1,18 @@ -\section{Dokumenttypen und Icons} +\section{Typ-Icons} \label{sec:type-icons} -% TODO: PBI 9549 — Typ-Mapping aus Unterordner, .resx-Lokalisierung -% Abbildung~\ref{fig:document-type-mapping}, \ref{fig:document-types-resx} +\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. + +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. + +\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. + +\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. diff --git a/chapters/execution/url-files.tex b/chapters/execution/url-files.tex index d4482ec..3341b6c 100644 --- a/chapters/execution/url-files.tex +++ b/chapters/execution/url-files.tex @@ -1,6 +1,38 @@ -\section{Support für URL-Dateien} +\section{Unterstützung von URL-Dateien} \label{sec:url-files} -% TODO: PBI 9301 — .url-Dateien parsen, Verlinkung beliebiger URLs (Teams, SharePoint) -% Zum Parsen einfach schnell einen eigenen INI Parser selber geschrieben als auf eine 3rd party dependency einzubinden. -% Abbildung~\ref{fig:url-file-parsing} +\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. + +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}). + +\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. + +\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. + +\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. + +\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. + +\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. diff --git a/figures/diagrams/module-components.mmd b/figures/diagrams/module-components.mmd index 22f65b0..4447732 100644 --- a/figures/diagrams/module-components.mmd +++ b/figures/diagrams/module-components.mmd @@ -1,29 +1,39 @@ -classDiagram - class DocumentsPage { - +OnGetAsync() - +OnGetDownloadAsync() - +OnGetZipAsync() - } - class DocumentsService { - +ListDocumentsAsync(orgId, search, type, token) - +GetDownloadUrlAsync(orgId, key) - +GetZipStreamAsync(orgId, keys) - +ResolveCustomerPrefixAsync(orgId, orgName) - } - class S3DocumentsClient { - -IAmazonS3 _s3 - +ListObjectsAsync(prefix, delimiter, token) - +GetObjectMetadataAsync(key) - +GetPresignedUrlAsync(key) - } - class OrgSlug { - +Slugify(name) string - } - class S3Settings { - +BucketName string - +ServiceUrl string - } - DocumentsPage --> DocumentsService - DocumentsService --> S3DocumentsClient - DocumentsService --> OrgSlug - S3DocumentsClient --> S3Settings +flowchart TB + subgraph Pages["Pages"] + P1["Documents.cshtml
+ .cshtml.cs
Liste, Suche, Filter, Seiten"] + P2["Document/Download.cshtml.cs
Einzel- und ZIP-Download"] + P3["Document/Share.cshtml.cs
OpenGraph-Vorschau"] + end + + subgraph Services["Services"] + S["DocumentsService
Auflisten, Filtern, Aufloesen
des Kundenordners
"] + C["S3DocumentsClient
Kapselung von IAmazonS3"] + end + + subgraph Model["Model/Documents"] + M1["DocumentType
Enum + Ableitung
aus Ordnername
"] + M2["DocumentName
Anzeigename,
.url-Erkennung
"] + M3["DocumentKey /
RelativePath
validierte Pfade"] + M4["OrgSlug
Normalisierung des
Firmennamens
"] + M5["DocumentViewModel"] + end + + EXT["IAmazonS3
(AWS SDK)"] + CFG["S3Settings
keyed service"] + + P1 --> S + P2 --> S + P3 --> S + S --> C + S --> M1 + S --> M2 + S --> M4 + S --> M5 + P2 --> M3 + C --> EXT + C --> CFG + + style S fill:#dae8fc,stroke:#6c8ebf + style C fill:#dae8fc,stroke:#6c8ebf + style EXT fill:#f5f5f5,stroke:#999 + style CFG fill:#f5f5f5,stroke:#999 diff --git a/figures/diagrams/module-components.pdf b/figures/diagrams/module-components.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ba1f3432e637266389517fdeac468eef7d446ac3 GIT binary patch literal 34006 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 literal 0 HcmV?d00001 diff --git a/figures/diagrams/race-condition-a.mmd b/figures/diagrams/race-condition-a.mmd index 301570c..f8e8e56 100644 --- a/figures/diagrams/race-condition-a.mmd +++ b/figures/diagrams/race-condition-a.mmd @@ -1,15 +1,21 @@ sequenceDiagram - participant A as Request A + autonumber + participant A as Anfrage A participant S3 as S3-Speicher - participant B as Request B + participant B as Anfrage B - Note over A,B: Ordner AltName/ mit 200 Objekten, Zielname: Kunde GmbH/ - A->>S3: CanClaim(Kunde GmbH) → Missing → true - B->>S3: CanClaim(Kunde GmbH) → Missing → true - A->>S3: Kopiere 200 Objekte nach Kunde GmbH/ - B->>S3: Kopiere Objekte #1–60 nach Kunde GmbH/ - A->>S3: DeleteObjects(AltName/*) ✓ - B->>S3: Kopie #61 → NoSuchKey ✗ - B->>S3: catch: DeleteObjects(copiedKeys #1–60) - Note over S3: 60 Objekte aus Kunde GmbH/ gelöscht! - Note over S3: AltName/ existiert nicht mehr → Datenverlust + Note over A,B: Ausgangslage: Ordner "AltName/" mit 200 Objekten,
Zielname "Kunde GmbH/". Beide Anfragen gehoeren derselben Org. + + A->>S3: CanClaim("Kunde GmbH") → frei + B->>S3: CanClaim("Kunde GmbH") → frei + Note over A,B: Beide halten den Zielnamen fuer beanspruchbar + + A->>S3: kopiert alle 200 Objekte + B->>S3: kopiert Objekte 1 bis 60 + A->>S3: DeleteObjects("AltName/*") + Note over S3: Quelle ist jetzt geloescht + + B->>S3: kopiert Objekt 61 → NoSuchKey + B->>S3: catch: DeleteObjects(eigene 60 Kopien) + + Note over S3: Die 60 geloeschten Objekte sind genau die,
die A erfolgreich geschrieben hatte.
Die Quelle existiert nicht mehr — Datenverlust. diff --git a/figures/diagrams/race-condition-a.pdf b/figures/diagrams/race-condition-a.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4b745615c6a7c1a4fd458796d3fed906e007aadf GIT binary patch literal 28607 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 Dv6xob literal 0 HcmV?d00001 diff --git a/figures/diagrams/zip-stream.mmd b/figures/diagrams/zip-stream.mmd index 6b0af7a..9977cdd 100644 --- a/figures/diagrams/zip-stream.mmd +++ b/figures/diagrams/zip-stream.mmd @@ -1,15 +1,21 @@ sequenceDiagram - participant B as Browser - participant P as PageModel + autonumber + actor U as Benutzer + participant P as Download-PageModel participant S as DocumentsService + participant Z as ZipArchive participant S3 as S3-Speicher - B->>P: POST /documents?handler=Zip&keys=[...] - P->>S: GetZipStreamAsync(orgId, keys) - loop je Dokument - S->>S3: GetObjectAsync(key) - S3-->>S: Stream - S->>S: ZipArchive.CreateEntry + CopyToAsync + U->>P: Auswahl + Klick auf
"Als ZIP herunterladen" + P->>S: Schluessel gegen Kundenpraefix pruefen + S-->>P: validierte Schluessel + P->>U: Antwort-Header:
application/zip + loop je ausgewaehltes Dokument + S->>S3: GetObject(key) + S3-->>S: Inhalts-Stream + S->>Z: Eintrag mit relativem Pfad anlegen + Z->>U: komprimierte Bytes
direkt in die Antwort end - S-->>P: ZipStream - P-->>B: application/zip (streaming) + Z->>U: Archiv abschliessen + + Note over Z,U: Kein Zwischenspeichern im
Arbeitsspeicher oder auf Platte diff --git a/figures/diagrams/zip-stream.pdf b/figures/diagrams/zip-stream.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c8efe7ac54e3e15dd64b8a6c21c86ed637eb0e50 GIT binary patch literal 40708 zcmY!laBIV>5*q4R4PSUmqQXoXqT01^3kC?EDx_1&`Fkl$_M!V&{ya{M=N1LsLUN0}xaQ zPE1QI%7kjxKLx)MJ3Fq_ycAFX7$X8AIZ?roOW(;?LEkUGC^s<& zp~wWG$k`F32%^--S;2ry-^U9~hJ(a?eH2W%^n)E0N{UKTv72Q^lvzex`b3yzPLx@O zdZt8~W{EJ(EiB#^~6PuEPEM0jmz@2mTjZw?v%DNL>5PjgiA*^JMh*!th{#e&C|IgbWC{`m9w^!oZtQ^Rzt8J&Im z|J**k|KH({&!^YdKQuma=lkw8uirnHk3V$r-|xHm{ilDNo*!5DG1~6mm-@K6kN5w7 zpI`r9zW(R_`TrmPc&cCj@%;V&+vDX+{=5pm|M%DLIw4i7plfAa_WS;vKD~d>pWml# zbS?@CwHv4HXehYm5#qF;O}jwU;$c;tq8R^ZNW{OOBk++NyZ{3Ply>(qeXj)!NOe7q6iHQ|DJ z7GKme#;la5&MNZ^uZT#SeOnZ;XWd@W<6n<0K5}D@Tw3`anSuq|Htyzst}}7=@2KaN z>tAQjF5XcyuWw84snWuoM@-fTZq~NRExz5!&yjLlW1-u@1>1Xa@iC*v@t^O^?FwjHwrJ0mXBvDZ`G4h?W&S;M1!v|m46 zZ&JT;Vej#kky}?EUfmn37!=&G+{)(a!$_-){>g?JeJN zCg1sdS3a)pqyNtp%a3J0_}Xs2FNS%R|JzHSXL=;r1+7`U`Om}S)8mCU{(1G|@c*ib z#?NI;PdOf!|9AYO_o9N(C&r%ZG}YEGh&!0B6S(oww}6@Xd{e)yd-OX_@ce=~nr`cN zoSS$~O0Ry+quuW3Cl;^&aaO+Sbj@zX1+$KYPyS)`Gi?{ytPPg{G^zJ;x*|ASghTdDaf-gT~@BI3URr|vx zEH&S9?{56mJU1@ai%wmielELm{kqqC-IuRoeV<=ViEB4`!0Xt4!rfvX-%icbomZ!v zTpy^%8?s!BzCvj^LH0Aq|UCN9pHQa>XW;L_q6{M+^hX_ zGm(CLNrt#=#U`YKmj$nO0r760{l@2`1HA4FP~EBx8Zb;w-$ z#nJCPUy3G87M^a-R<&Plv9Xe$<9?efyJWW>EipfL+FbcCf6uMF^{ej5di}RLyi+G} zMfpOTE4u^OzUD1;kPVP@(k_F`4*sqB||(&bO{ZkIjGyB+ts z?DnqHW%svz+&qErV&1i-H~WhB{r>19_UDMkjk-=XlXsmbCsz0Ddb+W&akIcKzN$5W zhI>O}qW9j7Y36>q@aoS07cOkE;@&%J(e~WmJhsn&wS{e8yIA*))a`AxRr5CQyY^~Z z;pJD`GB3Z{_VVkQJ-K(cSUtao+Wa{yuo{_T+#5zwBTB|L*ud-z?s53WhdtD z4;J{ft?KUnWd&PuEAP1J{oXav;@Z&@{h70#svno`j9<2LL3MJ}%eciW`X21~QpFOx zD1FUgrDuyngII5VFZ+D^@YQwKd!J;ze;sz|je{XaLh_1qPE%2fwA;tl%Y8kvApPnQ z(RG`1C&#RAu+Np=Uv=*Cwjf>Q6k9vPDZx{VK5A_9UHV;T&-5*8cYZOGID2W^mTSQo zd;6r!C!gMzr{4E9F46tVxyQ?ow7x9iORitWemPB8d;`R@7h z?aDVw!UN^%_|BFuK7G6R&*R1X{Xg@5)VzN%SN=Qy!T&EGKfcJG=bW^&YvI|)MgDGG zsdw7z{{P#Ud%k=_%rf1{oS$b{RVpprqheIL$TJ~Uc&3zkt5ULD@MFE{HpNYPzOQxk zOVz~Ax;so)%HZ-=3%=gx^3=$qrb=x4 zdGCJ_`xkqx@z$Cg^L^eVgQ=eDW{32KYfX+&o1D|>!|=sL&0>kVk#72T7quNhC$1FE zS6IpZx1B|yUvvpqWr*Thj%bFU346Bw+HgAhi`KaiHSd(BxQFhI^B$+iRLzW+E%PjA z*}cAc@0Zekfe(j@XU=h$cWO()+2ENIX55>lJo)AGR7<ump1 zb2q!}XJzlw>mJ9K|A=`0Vq;y(hZT?I0*~t+x}2dPoAt6i|Gt7vCHt@MKA#-rBJ3`l znrZ%?e_qO#2^Wnfg+1F>TK~aB{YrD?bcNmC4$nk1PDg*|I&nN|%BkY*4p;B(3U_e2 zv1sZvYqm|VJkt~oC$+nsQu2;^#P&vDyN6OjpS+6W_QOScX6V4`wb41!d z>q=*DJpb$1TgUr5tR0esT4aJ|tSMEvdU96a^S_SA-yU?lr~W75Uge)HH_m@qE8ViM zwBBI;#^u!ob3}gqy7%Cs+qBBv49a^M*SD;gKUMzcl&0k(HcKD8_vYGnQy@_8bWe?` zOHG6p*@sL87hwsKj`e*QaQ|CS>PulLWhdvf=VaPI_x zc@tT#d_1G7FjL)9QQlVi)XdkB!RJ0q3pOoYo#MN;Xl+A??qv~=S4>|Uzvy4q^bj_- zZLTp2usdO6-1&vYG&tx)3iH|4Ip)FXC%TM9-&+8 z1hf5%OL;K6*Zb?noHO(7^=i|S|4lz}Oy}o?f5jf#3g5hFnbfzUcc1;f4xZ2Lr`JrM zc_-wsm{E>7W5G;SD>F;8wGD|~$yYb-V$usfxj~9|bs;Oy<=WuyzNsL3^}Fb$q6IUr z)~(I<6U&={Z;E4PV+oD$6EAWPulRZUxWC8(`_@iSxYumSKjQ9EbD0SZ}68+ zyTw$&x+iZ&qWcH)ru~L_Gcud^3;yZdl(>xdpV5!gTN2yOKG1L8KgaCMmV@zZ^^xXh zHVFTiec--``I#*T@3Z_Klt={;@?99VF*3J7R|D4S{>?8f>?1TJkyv8|<^{juU z?f{w9B*i+B*EsisxQM36ErY{sfm{o`7*jMG^jHO!F`N?V%A1kcY{43^oZ*v*!+a)= z5QbJRE$Osd5A;`rtk{y+#_J^Nu#(9mlwmEGLKx!}t&qD0(`6mkE4>J1+1s@sT=0ul zQ=IE0UgO;F=1p-&9i-E4zyvR>=BN^NT%UA-*Ek2thUqdq)i%@Y2X~d|OtUi^&&uO#mO}YRh9xVWe0gCcKa3o&PP6Im!p*HD) zcG_Ko!^ql`E@=CK(?C;P;WA9a*I*j11dgp2qT-l_>+MQx<3)D&g77sk! zEm-$@O$DblA!H{l1^L+cMJU_eQwzc+zi2hbrCwO=1NL#+h1D_;AA<#Xs-`-wH+m7u z2k`-z4b$Zg^??z@$1q{AzB5?Vt3%ZnL3|7|8m!L};$tuy<{%xYkDo$(3=;RE6YBS7_vFK_VB)^$l@TL6Ix^#mjNM2%MM#j@-}- z+PvTpTDl;ds|rR)K_VAfxd}9KmqH>Jp_Z#^DI{`HgkX_-7Sr%qkjO0@*XOkc54nuj89dE8xs$2haeayB?xt+!y+IeWS>eRmU5R?To8m$* ztmdi;bzHCYB9wLSDsZu!&$@S2BRH=^1Q&#hep%HNcNJ1DgV``$SyR9@+M`LL$hn;q8ZvG=6I)UeTIwccfvCb z-Ia@HSbNQ|@tR@lHN(zphP~Ge2d^29UXJBAbP|L#6U8(Wr8E=eG!vCH6V)_bZzhHv zkVr7oG<>@u@<2<_p`M^aQ-Ti72|BbS=+K&=q`Qfm8#Ca%s!Xr7b6yw%lAg zC8a%*F}Xo<*_6!ovrK9;h0xM$>e&B*ndk>@od-)lyJ z*Nj3hCF?U>63l0;mO5GIh#7pGV(@v2!PhAU-=`S-oMP~MO4qv$kq1~p4)KH>5(zmZ z6LLr;$`yUduijpNCk5|UxvC%e)jP^I>(Qi${VCV< zSAO-*`#ULSf68_JmtVc>{!U8RpK?QA^P6|y-$^O^Q*P>Ke)FF9cT&dwlw0~Mzs<=0 ze>`lBd&u1CtIA=s4r~&&dehF?y~F=d?KGz&wJhHQTXb1J*6!H*H|O195!r2`9N#^6 z^eg0Os@!2uI>2}*j+@2sfk5H>mft$99ltr=E#`>kl4$NQ?0E4l>VSkxkE6@>>rBTK zl6WNU#&HQIACO>n0|_)3vi0PxXYFx3B%xALt((9jbWCC4?QF*62?lJEcjLG?lP4I0 z7~B%gJ%%U1W^o*IxbSvsgP}lA;{uRM1&QXSJbl)K5+Xg0jvzA^B=B(U+AC(j)@2v~ zHd7>d0a)h(HKS7xq8xlvuDG~K-rux)30Xvg_outm4E<+lTm5Ll(p zicZed0&SN&?gjF$H-ZjGEKpOMa&T>fp@XlFMsqY{asx=`b*8K&CuD)`Ove~Z4Hxp{ zvGy=#B{@OtI?`gaLo7g87)3zTfDK|r0#AUj@Tmu3P%E~A3`Q0}w-aW?I@T*kpw5Tc z+2QEsdq$%%WsZD)0_^~DzHhW7U(WeOS^KU$7qMB(mUlH{Yp8SAWMy*mZm}d zbw;B(J2B&t{OwOa&OD5=z276R*Svn}=ff8oHBAieZqV8{?J&R3tQjq#HM-665_2Vv zYVDi0KVG7jd9mQ?A5q`z3_i~a-O#oE={xad=d#ynqIHhWrJap;;| zcDaW7yL>O|UrDWeo2q)a^I^)$y=y8z=4#m$G)}vf*P5{-Wyh8)anmIHc$a(UZeR3M z@NB=t^joKm>=*qMOkKHG=X3K}{$)$IMd#FQ_<1lBDsXM-w&;_8Jf69CdB07aDSz^h z$EGWB(|%4o+wbF@yL}UgF=rackWD`)macp|ZH9bOokvWT{^>skV(b%_e%o}0Ke_~0!thkk>15yU2PDrcUj`ZX{m4vts8Ja<{cF5+`?ua^ zPKO(xVbkw9ElMtXEBm<$Ib+?eHrF!z=lo2B%g-oX`E;Wisuh7LE1zy;1Bq?C5Og~E z?N<~lT&G9LWv>Mp2X)cK3b{~_6{|sF+8`@JP^^dmS<&|E(8iB*dY|Xmq(8QlK0gy= zMOxK^p64T5HQ98~emigSS=3l_4Ut2pQ5t=n&UK{&lm;{45ubH|F! z<{7^ZI-Pv>^8rbiz~+y0ChKgqxn{vX=i?l(pUuDy0^5AE;#?%i2Qxv2fwXT1IZFo= z3%hL!juq=|x$-GuvrXZ%c91yO1>&(;m1_d!EIG+bSMiDcE)8Q$(;$ z!LiRNHvUjTBz5J}jcpYVdY*TJl$BLHI>Qe&%4nYYwWZUJM?Gvbh8u4*Z@CI6?hi7j z+xX7}g;taLxjzPQ`BNY(m{F}z0eSkfb-}UEA)rXOR*r1N@7$MG`OQ!Q91$=0=6tkakIJfCb9OE`1AmH`Y+aaT0Secn zvKEgk&E};;i9V2;{4GHy<^3SAS-fZEgtV=0r@%;=Ihf|P+up4Pp^FiPECgMxNjB5LQN4mW-DWz-&!#KD4g%Y3IL{lD zHfCLJG=^wrvS0Rl{Yy}&CWFlQIR8rTb7ZF)&hrL&IqDKj16XneDF5{F&G{Ik1obGSg^ZaN<9GGa*+iG*Ij&IJ# zJrd`;p@a)4kiN=TJg!Ux1=7bk7M~A8mG?Yv1?8b-#Sg%`p@izmpwr2`&ksnR_g=Pi z+VNXwJB{t3Qb#M|Hi4|jLbaj;M>Hiqc&R_6T*#zN! zC@T=;sGT53tpw@F2I+vhSK_?)Rgkr(xR)O*Hl4TnV@&V!ESqqU$D>X)8qdpK07@pO zI=$PxK_M_%czN4pP`sXOG=?jFyweS2!9*F0$2*NcA?pSX+3?3ZrOsciSO+e?Corem zzyyvJ`+}nGw?)CR;AY|#p$hVXBPfh`K~Ac+fEH!X3ZO+p@bBuDBD)>py&BiP*Jw5 z09?+%%mXD?sY8v%aN|wqT?eIa(`M#$oAuzLj1^qYz^p$D$`I11R%n4ji5nyq1uDwe zP^`EPD#}djLB>H{lx4FXRFs+4F+aa)2rkNgtDXDvK}7QW#E&YH=O=wMkvubbX5Saq zI`N_z3=(NYYb?UT4j5eBFwf9=g3)73<0qcRPa=(DL05i5AS97vnrH)_Go>^SpTHc?r(*5}hN% z52RL2;;o#Z{;c8%5%Jxmv}3$ z@ruSWPJh{H{$!^4la=OAcA7sqY5wG<`I1dcZ7=s#vhjsRF;0K!Y4Ie|;z_2(lS+#x zofc1KT7;CcmIzOJr|_EZ(mRFsd_wOPKJx{=SNP61`Mtt#y$f7Bd^`>`^VoPCY?iU{ zIMl3T<8ioHx6#Gs#F6F@n-fQyQ*2HgYc8=lalE<3=ERBSDK;lgHZQR`ajJQX&56^^ zM{G`lwroZ=#u-3tXr1-7Q4Is1>eG# z`&_^G-c>YM{QKBD>D|s5yYk=c+X#}Lc4zw&khJP|-JKxeleMSc3CsYI(PixyO73s^ zJ?*Zd$zAip`$uG*F4qcw_q_w*c3HD5+mU~?R{Nd6tas6c_q#x*Eqgb67s!-X`&NRu z&!P_LDP-)LwjrBycZ1Hd-(Y@s=04Tylb$ebn(^eulZZlw87DVBi!Nexyu4Lq=QI`Z z2}hxxEx8}#J&F57(yshb^3FIn*Y3;Fx9h7J@4t!O|JC;X zFW;X}CiP#qSI#K^tM-rbp6{YI_O7?>M1CJDWve>hx=pa~#3iT2R_i9!8?W1B@bue^ z3wHItU(Y_h|8Iic$GZ#P+5MUQo^R^X@FknuTrNd_T|A#_-`}cFf1h{c@8|e)_wfB_ zy>i9M8=r5#u>0ru^2_Ym#do$Jzx#dK`@cJWn|-bIcyaiCG~e;a=v9|A-!{H`Ust_G zdd{O>ySLNLr!Kay@jM>?Z|%wH9aED3NbfFN8y&MF?(N^RThv>=7wlSlM*14Fy zlle2N?#b-+8}08ke1DtBvUfN4(eGa)tXgjw?=-vl)GO()+1)RM z;rk`scfPg!miGAhO})P6!h~067rxut^}glPySBRe(Cv4hb2~Tm-+3=5nRj~6`>8cY z?tDJnccty@{`!+|Rm1;!{=8Vf{oh~fG``1mUq6UGDdNyg|6lfe*SU*ElD917t-_a@ zP5bxp*Z)YP_&W6Po!$bO0xI=W=(+jZ%e1a1dDo#L}_Y0ZjxB3aSXWKPX= z&f>ffbuJ{SdYa^ylxK6CZ^xdKn*2=pit&>fUY4^?1Vyiqd=>foa?IJ~6Tax{nQDq< zS}$8|Bx}gCXIUKwZ*H)Cmd%vkJA-VK-aPl;_*Uk3=+<)CqWwX3CSO)xzhhR~|7u~0 zrPS?h?`n2G2}<`!dX;F^)pug6uI`MDa!bERY`;ElndJBL`mR;QTg3a8t?vAEkIcblcRr8g9UuV~Sed{>y zit)23sdt}(Uw{1R`sK;LP`mE-DaM&;nsR0n&7S$>UKhN-E#1y^s>}vOmHA7y)_W#w zo4#bS_ILi-QJcBu2Q5wec1_%F<&$e`_x5jzKa-SMse5Cl%AC-ZPq^;-9h%uZO<+U4 zmxZ(RYhByO7dn$dv`eZM8fWIOKiOC2SmJhdTS>jp%I#Axcy8Uzxa9QLEHlR{FZH+2 z#n-P1H}WUFF##d=UZj`d=CG2#qVkBM9$nSG2Il`DCPL@-Z2$f zvnKIs?djKch0pjbxGqfW+3H@m<}Yb|BCj95y*7KMcVfnU|Gkx(F}^vr4==yq+{+&| z|EI|>pH-X73+?QB?;ZX7X{~&n)L(;V$z|95!^^L7-2HWVL4MBO%aR{nKGHv0WD#l? z8x}$UdcjX=43DZAIca{6?|6HLi zXRdkn#_i*Gc{CEg+obp1mt{XbDQ0({^veY0*MXcbqS&X({|yYBXa4^A+256)>+bED z4`u#~zQ1Sx-Tp^^*3FlAx~ERp`)#|F?&payXZrsc)cybc;jP?udzQby-Ywsw|0m?{dV%?TmGj0|3%f`zr20@U*wmt{nh8+zX)U|U7D9`{&D|2nRwfl zXXkgWTsvdai@RTE|9-xh-~R2M>OCU+_uo$ZckJ}ij%VLnerwy+{_N^^m$&=**gmQL zLHXa?$Gw-u_W$2it6aAKNAAr1so6GB-@f<%Z7nWbmZ+Pjk zq-fa{`!83Fs*_H>{q@r4%C*+EGs^?=)|(ZC|G47vR(Hd7+j5b6Ru`t%b}sC6sowj1 zPIl{^whf_0nZ;AI6usYWX7gRR*Guct%2{6XV_KYMX3w_Vx;Et2l5H=WLh>|Um3aBq zCMc&rl}O6CUG(T%*0F$d5v|Ye(|Ibg@Smotm9pr{g=fp9j{j=exhm^KGS?OL zZ42kQX8FphzirsaSZp239_Crw7Ic2bt=Xz)fBe~W<(FKYi2bKhtNl&;K9@!oEUozZ z?&62bwg3NYayhb?W#Q(6a|+Q9?DwxZ`!KhK*F5@jM{WNVwq-{;HyX_~OgUSanU!bW zIpuih8RJhgg@Wa-7sg7@S6N#B=t^xzy5hB{Pp|T9W@hibGq;g5ojXsfNAOMZgP3(? zpCa0~*vZX7Y1Y#q(}T-?&h+Y>}(;K{?0tC(G7!_4=uA-KrC^aq`v8 zS58dXtA8zc#gc7mx25;a)Aa6qYp(I)&exp6?0oG{ZtK%m7%urHJjJ}q*7-!B{(|7u z^N!3~WV9*Z_#Ce2y`r}m?ycH$_MybRX9afopUdoao8^}j9Ey^?^EE90cXY~{tG0Fb zUPxr-f4vuWP3OX!3r!c^UAUF^?H`lC4M9G3^_S}L`YYv=<5tDnRj|bQJ-1bwYbdr{ z&RRFyp?V5;@`t^yR>^x8R=p05o3&+Y+1?`z)eWVBH%{-qT-3Jo_1V>pc3IPN_f{4P z{Mvi;(94G{l9zTK5&OxeQj!)mduERQ+t)SmE(fMEK1sS~yl3uj+2f+@DH&P9udeP% zOjKt->MUxK=(2IoyJ;SV0k=b&e5d=aIqdq%Y@Gyi+du2focF)DI}b~KHZrNW9%Cc( zu*#uM(%x2Fe2(*rK)-3`o02bk)=s-wxA5fDTqC(l?&;f~+7?d__NeM*J3 z_s+YIKdJ}u+sE&!7x^BaqV%^^U3nYd#=X8aY?-xd_ompKKg7Ryf;4Zvgn_op4fU`a z%f-cR=xlKjyx(3Ze=jji;fA^hRIK#QKK}!?hxlf*9+D6^*6@jMGt-d;uOHrduBEdf zFysCC_F9SW4`tI>I~;eX{af#%g44y*wWw_lMp<8A_t zyMN%@%ydj4nWyMx;%3nJeDZ|De6w3y3_T>8r#$*AITaGmtu!WT0&=CMJj#!`J5@-%lWD8Gh>~;D*i}#Ii z!Tb$}(;OAqbS2Myzqse{8U{lT)}#Vau^T!f2ca(SX-wc*@@7M%0EnUTW<%r&3D9go zC1@gNf&tsgcN-!-B$`_cJ@y=4(_om;!#G{mjJpTKXqPqP7EBf}WHWqkpq;?ObW8!{ zN5*6U1GYx6M_LRQY&x9QXu#HDxM0I!w}rK5&whKzWw?Wj`wW)=b6Cg0(1?W13z~i# zygLkXIx{4=(iW=RQGZ~U*nYMtfrlko0pb!vww>=_ZUei-L!vpP?1qjCX!c{thQn!$ z$2)r-yBsT2dTcSVaLx$}zZ4mDqds9tl7yc>*~WW4`FDue_Vu&2RlfcBe7EHLkI%nx zP77XI>!7~%&BI&AHO)CwYU}=-e8!}{wmxc6X;$UC?V{VVGxuu-JpB01aN3u7nMcBF z(*%;d*Z8}9^|(^^=7pBwJSmattO@)7TzPO-*>=9P4K`HsWT)ej?>6Qc>!)VDjBETX z>Gk2vdIkBK0)hKqwak`oj8c1IvPOUH@~R5a6K|_l{VsR6`=g?8;jG-p-Mf#*Y~R12 z{^(S-^17Dz`oy)tHP^FG-uiWRo~ne(+@(&39{l~Z`nJfb+u{wiM^jVy_jkUst9-NV z{hhDp1>XO9{#NPu?a80bD^4&;#{cYiT6Hn!w`swite@ZIXG(n%55JBOexr{g@+am)?rR zl%FN~srhC~tAE9GM-BP8qWfO!O3(DFFL$ce6xsUiF=f|?eXvHt%Y~))%x`=&-Kh*ZG(5V|NrIHp8kHn=$Aq#-!!Xz!Ge_{HB5qwk8G|t z+pwiIPwA~-@_gxWNtoBC+_H+B~+;;EI7w%U3_fXyK=i~Y9RmYQh{w;m}P4S{d+pp4Tbu*S$sLtVC zGIf#8?~j}0_d8W}7VHlw*dG@)F^ntoroPVa)^HE)iI3{06xyF!KOyeW4wJiI|E{~# zHD~EJ(c(M()BpYuz2q$+9@KL>G-_hlr7ds7r~N&oy<{rau~x2QPqo&1=<60vRMiWL zS6r*&9h1`8{Xu)l^l3YusOs(9;}oT-UwG0xrl_KIOHkCKNz-;L`NQ>csjA+dBdxEs zmrSkbJ@f+^Q@l`#^oJzNND!#z9d9;7 zCV&_gZ#G1BfTu#i#ejeT+roDnA~__Q8w@$X#ehN&<5XEQ?iNFZ9>!K#Gp;B_wkU83 z7|ED)AujCw?~J>N$YqQ!xJV|mi~*O)55V>w__+Mhhv335D=mJ6$W&|b*^0W)>tgml zTDbhthTy_0D=l7x$P{bwS&F*P>0?(HEj)lTl+QU)PbGzHg3DGX)<+lidX&L z>AQ}9i<&v-ap0D}v!ZVW{dg^zeX3t2cwgy)=gQZxB7WN>-S?G8w5A>tzm_!3`1_Y* zA17?B`=Ypd-*d%mCAsC@uFoEa?ycs(`*zmiP z>pwfr?%88|{4sxu{J*X%>|zV9#X2 zMJzSHx3_S6(3^jAMazww`D=E(EZg_#gU{RiUyHuoK0ojNx>dgqgm=8XYgKOhF!ue0 zOxuUQE||2JJUU%1S8w@`{n@VVd*43n{PuXi?H&17uWQ8)oD&RDW_`cofBgPMFQk0~ zw6~j2Jo5Ui!Py^N?^`o>uKHW_fuZ*igc(eF7w?9}(2?|hHMbXVIabKm&m*`Q;^u6=jzOJ%Kg+yDE;mFf4*AD&zu zkQ;pe%01s7OU>Kvoy(N_|Lodf|HpFmdrE)*n)$oGuy${Ldu@Wzzpwjbzva$f`*o%F zM8i_ePoc~78lUPI2K;>O7weR_OMKOzx6AYzvuujjz3g3H)_*qkfyeoir;9&*nzP47 zT_1@lKWontc6I&8bJrBiDt}7myPt9WRJiQh+hxIw2^Q zaE8A_w#}AWj-Kd4bEXu8W$N$0P&G}!OS64S#WjtMTdI9+i&tD~sLATxXp$hE9n8Ee zc&BdZrLv0)k2!nUc`v%~{i<iC5V+qW`L zciuT)`>M0aH|-bCd*7~7*^*qeKMXWplrO&cPVJ;`ihYwbwuPV5E`NXf-qLLi1>L>N z#MDl?dv9Ak(b{Nh#SKd;G=k`SD&^ z`Zc-vuinqT{dvJHyO%uH$0l=eU;irdkSF98m+cM-tL6|*E^*zjJv*oM6@Pc}_X}0z z2|BlA=2C|pF0=TS?_19v_G#rxleb^hCe%y{>*exQU3vHIj%7;&Tm4rC8CSpNo*z>5 z;%-`rU}t*Y(!H~oswTB;)|wi#@U*8zaYkqV1yj{Iid)Ot^>j+aJm0EvUc26UZb`VZ zq-x8#6;jPQcLc3gdhNbzdAextJ4U9QDcy_b$1ezab#))RjLVkoMH?<3bzbr%jPLr( zXRTY=@4Vf|J6(ULc~Ob^Qr*34YF*eKPB1%}ldspf!851#PSm%qd^?w4d$-lbJY>D) zxMFMJt6byRt1TC7b;-FAvs-dqp5bnXYuhGot9q>!dHY~=cKqB`r5|H%g>LB&+2ip2 zwC>*KC4mVgm-b~X*n6JQs(9M7+RW>`ckQ0bsoOQkS;jwfQF>a#y6^4VDW`Xu2U<2v z`S~d8T}&z0_OpIP_b)zW@PBydq4;B_WO3dgmUoRAkqQoXbYc{m1tJ7|vY7t;6LXpZET#RIa<%G2i?VjJf|>?~m_)&D+cWX*kJ#+I?Sc`p3)mT~EH- zM#yT11TUKyV>55M2oMZAZ=JNt)!y#G&p`k(Ri)1DI0_FU)& zQ|LxrBgF31kjjEo1^s};^i(eWpw#00(xT+lVg(x;(9YrH;M5X@Xz-?C{gBj(5(RzF z+{E-$Cn)Uxb)pk6hQlZK`b)`&~9B2%Ul7p z*EYz`j!WMyKd%I2nVYGCse*w*5SPB2g@Of$WoHMH_f1X7OmxbxP>42AFiYZYch1i%NzE%MRsij@4dT)dE-5NaE(u610t*^&>4zjH6)WfmbLm6G6bwQ7K~_RT z0L%yNSq5#|h6h10$mG(z643Tz@6429g=kPn1;rwEFB^h9fuav$wQEjlE|-2VXuESB zM1)H}K*12|sep6^ut8k<-U^0DhJmDsHw+q*-U?txAbSQBV#J#R4t*|tWJehxISM3A zykQ7OfgFM4C?ima6K@VQB9R?sjN~YgH1USP9R+p-vZFxJK(skVpq>3es3B#7a1>aY zXu}KRH%dHi8c+%RUlU&xypp3 zv~CPa%;>H{D%HXAWVj0K3S?J-(izd_m|#ilhDgOaSe|Ip5MgBkas{%hKJxmF-}8qD@1Dl_|&- z$gTpVGosBg#gf{Mkji$jJkh2hxeDY8WLJUG8PVpLVoB{rNM$=%o@mpMTm^Ckva3Mp zjA(OAv7~k*q_Q0>Pqb-Bt^&CN$yH_~rFK&+soe;vYzNDe;VQ5zkX;2zXGEK0h9$Kd zA(ic5d7@22gq0b{709jvr8A<m<|L(dGi<3HscZ+!li@0`E0A3U zN@qlyV~#DgBbDu7d7@22gq1nS709jvr8A<5OP|%(0|)W2CYjEKjs)NUj380?AbtB&Bw9EUDcX zscZ+!li@0`E0A3UN@qlyV}T{LgAUIF4R3-B0n39J@F6|OKpc*tIdEzM4aix5T!HK= zkPOl0SYS)-NM$=%o@mn$F=zpD1(K`4GQ^vM#Z}P;(D6tGLvX57Fa$@l0(clJNC7gg zqyQPoQGkq{C_qLl6u<)jK?;x_xdOP08>9g4QwAx3J8?k@;9gUZ0=SzJqyX+01Sxm3OOOJ%xe%lPuAhSxz!hSU0=NbWQUDkEK?>k9 zHb?29QDP2J zVh&LPDGd}L*-rtI-ecjJ8Ke{x8t}{v8ORSQN=@a`_bbgU22BZo!we;cLBWfn4N?Yx zv<2nomw;mv9E7l$0g#MeYDI~6Y9(mYA7P7v1;`1=5d_LLC}uk+mL%rnr-O`2Pc4Sd zF>vYoCKhFT=B4F>CN?0`D=-m=TxtsVNK?@K2CD1esUs{iwLG;bpeQvhwJ0?YIw1ow z3PXxZ-zBrSASbcXB|jN*{4D6aRFL7wX%8`MW&sL)s5c;|kQyo&ni`l{aOu0@IjPjr z+{hC7pwgw)+%+PumnyD)vH50t^LuknLi%P;26IKl*$s{3?_ra%b4b+z@HGKl5Pec5|C-R)bhpg%bUY4)aF#FAN1d3_9HF_GbQ@ z-?w)!+kr&J6V=%d_q4ewuL)x?U}IP;rZ?xh!a8Mn#s?7$Cn76iPxt;le!GhC#9D?M zMQ2Q%*wGWsJYjZtcc8TLR;jiPtN+3K z58r!CSYTed*5>uCiFJl2Vot}L&Pm^wwolFUN#Q5vpN+By`yQ0c>0ABqTDJK7+H0n> z#ycxaGr!f++Lo@4NPS{NHsjJt8Hgr}vn;Z-2K!oMKt`-AD1pX(=`{3NQU> zdRqCq=(gH#8>_&!Tfzq>Zd$?|5x|fj;c=jePvu10LxC11jTV8P#*YDv!V9I=HE{T} zMh39^78ENvex1mB_>zWxW6_Q&L4Hju4TN7UV4Nk~dF1*3*{{{JAEa~h7i`#nZpzYm z@`rjpUo^U%Ai0^>Cu;6pzFy56YrgFJ+uD10s=c{dFk5vU-*I2|X|wsFGyay|xD@~V zbVB2`v{MQBU3#4N%^LDwv@iWoYH!Z=+cM_Rl!JdYBXypiux;)=%q8}sco#@v!Kym_ zeSE7wd_5u`(NI<+d+TTJhDp7b?RobrEuB17$3Zff&CZOkd=mrH_o_wxtOd;M!HfuTkNM`xdI;CObxCZmll zqtSH(i`e0ijE18E?Jq^-{2f&_+5`=R+#EDxmKCb1ik6ST}Se<+W8_;1DtiI>k02K~JYqZ3pKp&8tosjGS!;V%o++^7@F zk>A7Qf9aO$-484bX~`UM zjoCpr`X8SCy!nv-N8cUt+YXo(*xRsQKj6%8M)%D20()atF=P2jW;>X}9MyAId=NCWA29)>e`KPQ~D^tru|xsKi3`L(3L^=Uuy1^n%KPbZ45Y5lr^V_FgWfto6h zN6{LI|JfG(vU{U!SHV(xl_7La*!`<9f~p5=PYPd^yuG>b0~=8JvAZrjGVkteGl=9Q&fYC!|PzN2K#gTvhl zEy)Ym^jjvhu}iW#e7N{KE#+zMjmz!UzXg6>FnB5Awfw{G69)IUPd!}of5n~y<%^77 zthV3x#v<9y?q$W_k6VwI%RTR@x3xO*{b%}AyU*zw^-7$@dEViPg@F zwW_sk_vP>N`?2rX|5cV@UqjDMKN|4)X#L;tv-ejoc$PomLv7K=iL;bFvi=nruKejg z>BG$bFMcaK=YNWp%YV9XdSU+gi2L3XZSHsPmw)DM(?9X9@9yaG$#*;DEH~Z`i+NeC z_vg-%=hJMqzu9^A{M#D84>sMtyRFJKcXw2Zm)SI*GR~-DD}A>7u)lo$tSwryT#Geq zPbIdByyJPFVl_W#UQ*ROt3M*aJ8bvNz4=GwTaNXf-kDoU--azKcs*(D?`>+^Hc88$ z`sgZmNcDB${i>LF%NyTkm*&l{oST1lQ)%{f)oX>W-%oxwT>Cxo@~vxIeU3-HiL9>B zHGVnoSM{_z&hJ+F{4l%UsXeX#%hn@@_f@Ez-=6X@;{KykLGfx;S!}H~rcNDKKBzqG zcKuZIb)wDv(5cF`o72L6riJx=x!Ij-DlyxlUvh(LmkXAST z^yCt^_S2I)4{Pze79=fwvU-)y*Vw4YSGOZO-L?9|jn+Rte#{=OeKa_G>Wv2(TQ6B`4V$>t z>-CkWqeeSUvp!z@aazUc^-WO+Q&^*#4pzjhUU-d5t#C`*!3qJBUlraq9S5hyJK9L( zAFSBJ`bbyz<1~wNeaE986?PnN{%FCb*x}g09{h*hVk=AY=Esg4=lZs*w09g&KmNF| z=lJ854{bI(?kl-=CL{OkmX*P3?rO1>N1tAu?z?8!mQ9Ol4*l?c@?l5!@i>!ZZw_5t z?By;d+NW_s-poYbTz$gZ@0BuP-ImTKQeUhZpGO>0c;d}s!%{pPFjk7)hzIsGTtudEz|%poOBr!H5a`r#3hQi}OjhZ#l%XTH^B22EQw9p&ogz z&WnX+#zY6c`}%t^>rdgSJr0(x2U$)9Jyh3VO4Zc8oVO_KpBu7e`~9^M&8I#%>@@oGZ1KFNKV6kxbqn@5ac0G=Ue41KT$|<*vLnXZ zA?rV%NDu2iyE3ySimVTdb?-NZh0fsQ@>}^e`)IV}ik_|8!`YUc`W8`r_?^q^ISnbC zTgqqpy!90|^lbE4+r+zDSbxnot*PH#M7?tq`#P5`i?duBwb6fG=xT$VQ!gE2Yki`n zVlML`{>9g5_6#Nab~e`;3qH(h`&;MBySn_e!E%NB>$mu=^WCO8loc`ae|;%$VQ6mU9!iK22SfP^9RyI`m1=N~ej^L1L?4XlbqC2r)S#(xa=j{8$JVvu~HO zLEV8}vm!*hmtDK0w=%CQxru4x&XsXqiL76>S1z&*ng}nZ%T@-S#En`7hTu5{oW4F)zt-Q`Sw* z+l`3BA_Rg-f>^ujMbeYGU&nD-*C+1Y_RVpawv7BfFS zu9dS+>B%b*#p{bcc&^x1aJ+wN)@2!oMjTs3G zmzkRpqNHiUK;uqo&edtqbn0_zoEN6BtX{Jf8t9-0Vs;BE@pW9Q=b4884Z9Q#UnEF5MJoW~lV|trPE- z?#ZS43Jr1(Gljceyv<5g^I7=d?u}bZr|6QWzO{nP6>OO^#0V}jTTLFa&gRMJ+ta7yH3}XpUbx2{j@c0WyS`D{dYfI zXG+*&%(-qxHZv2`kCcfIEe{Fr?Ninf;E>m`$=I40Iny+Sw^wJ=@3nQiujbpHIA{I! z!qX=T*S|eBTK8qj?7vRd^JhB7B}WC%VcOJcz;ymYm8?1=(|G}ocD`$(%L@-PaAww@ zx%%CzY_<8?!gp%xAIfcI`ks*u7#qH)Av9yd6shf=srpMj+_^5Ag^%h2*SAtugyezYt1yggYD+-Zst#U&MW)&lX$zA(et2n{LcqV z_g<5KA0?Q7`^L3BB|p!48_lcw8T$0yTIYz@dI?)T8||4Z>m2?u;HDo-BWu|D+8F8UQ+p+4(YdtX9Ept&IiWO?xFJKm^3C)Z>5d0YiW>stlDE8m6Q@6WVKMWIh)0uqzWG$A z)n1vGoW9QB_Qame%Gui%w{E^2*|+(syXB#(*NfNmu2S>y^*9?DAHM&Kjo8*(r%V=a zV%&dKKT>v&0n0`e|7_9zP1AYY-=3^nSy#L%-=4YR(Y$NA=U3g&iCO;cu(z3wL1eAV z-HwlQ=k9oRw0wtI>FwOxY}WIR%g=tk;_>6!&ia*aWM{qG&QS1qhuMW@*-xq4XBsoG z^Qq4dlz+;ZzOKK>>||#tv(S7_Z$33m2?1`E)QyFox2?>VdGVWlU-##a=j6Tye{K22 zFZ20A_;%y=e{1aaH)jW%uQ;#J|1CIHd*Zx{*-@c8%?u8i?3{7=^+5-g1Yf1)tNYhI z{lLnX|0t}mXQs34LkowL*NZ(I;^kRqt1RQ37L*!$sr0U<=2n9T{(o~5x&7|@clrNs zv^lUfH{`h83M+G_Lrq5v*G-P*cTVW5nRuW(;d@1e`Ubm`PuKJ}aoZkw`blX0QoC1M zg`I1+cuZhj=KMlb!cie-b&DaVs%LbA0AJ?9$2yHo&s`Eeh;PnIWMSKMFsm`(MN#Ci;F^+O7W6CY;}y9<2SsThV{VfgysA^F|6^ zx5X5TEd?Ah)8;-1c(`HmVKu&F88?e5nvYo8ZU3%Nxp0a9{>jASH@^s#yL|}B^qYE9 zWL8bTquTf!F%^l{M zVus4b%BQ_~)p9Sr6RHm{}yY!OA*`WAE06f14=lA`92{A(Wqx?Jn2~QujV1Ttkh&92ikYpue#IMHtQ1H$$n@aY?fg)n zcXo+y+Zx4PO5Sdtnb=<>|85?tG!w?=D@p_k#b8+~ptG-1XLT zzufrJdQ)xk@ynn6PA^Hn{YbUi-~aI~Kk>Md?-LUHtxj6o>oK!Q2_4_Tzw_$^#+d;s z=~ks_7aGk^XRPj*io6yqpZa!f$)r+yaqG>+GlHaebDCIxEoLl`&kOxt|6}5>SC4P> zOt0-oo?qem_j+gkRrw1)zIr~Haos*}ce!T8(f9(cd*7D)IusvppysRptMXU&SIsvn zNM3wdeTpGWLfbp7?{V*e=CgTeuNS8ot8++x4QwnhJJ~l$*0lW{fB4ILcm6-)jQD$W zO~OT`JxA(R&U+R5{7tf2eQ!?xuIRnO(--^LRXvWle>~=PJl}$??`s`ac*`W$Ke*=1 zct`uUkAa2ugGZLUH1lp~sN@W}8S)BA48ChL z9oTp$y;|(emRw>G#_z_|9C)5r?ubxrt;w;+B66oIpLEVuYbu>pHMuh(R(7suq;0QX z`q6t=dj1GW+OlNEnKVgSHBFiJGgHakkgkIL%DUn=|Cbh3280nb!4E{MpVT{n@3T zm!yCDzw7jUll7S=RZYcN&+dCLBlB!S0Vl(6joRyRYV4&vf?t?UZ(v*O5Ycya6SG_U zu3s9rqIz$n9GM}g>^7x$23JVbzv*X~oHO%xJdEi27qnma@0-QT%_h!V-alF2-`+25 zZR+y;nA%r`y8G^|KbO!}%IoR-6mtjzktH=y7sb9hwzx?8| zwqE2BJaz6wRoc{oo0Usd7azJY-}3gKqk7A)KI9VZFT2EIJ)zOL`}o778Yu>T?cC*vln`dRe+**U)xzFbW`t^Hc|_VxUi&u(^y znfTAlJUYiV_EpD;l!YfZt$2BjwfuZdMb?zmE-Cfv$4uU?@sLSoFJ?c$%eZ2)JO4u? zBNzUT36oDpN-n=F-D#z(Br&JQUMMxjORueXqq6wyqKnhJJ7eCQJvuMO=h(4l&z`&q zj*FZ2>~YEIXV=r7JZL?zA}IUBD*>lleh1xNJehlIjr+#ttIwk4cnz)6*zcd+baemj zpAWpN^Y{GTzB)WFqEdMB-S1%~>+)wNUJ3h_ZCd5|_R!2%hF>?I|1SUUO@ID=&J~ff z_8g7lY);r0!?@`1Iqs%dm6>tf)a&PwKb$o2huXfW_Gi9-lKCKJ=Qi8+ zPh#8!;d$l~9x7P{Klj{t&wXV|)Tz?4zSfWz7vJ&lq-0m$z0gp3>PZ!>!AD^%wBJ&2YW{jom0M?r-&3q07R@PZYjm*r6c3 z?6fkwvs>HEv$KEBX*fHRF+s&@u1M!A9Tj6C)pdO`Ij0=s`P7Own6p{!qolezuVAq9rX zo*RVn)y^|ZvP@iI!6Ex*fn(L?o1&ewbgP6qU&XfXz47>{pMF`z#;e`FaXpoWkB%zo z>)Br4en}{9?YGGnW}9$5_P^z_$*MhQHph#Kg1hUl@!YzyAkVV!)fMp{C%K>X?Vg_h z;NyJ#J*K-?DKJ{F7+-90{uTFWi~Y|J=Xe_oBpM`7?`8Fi(AF1udqVlJMWp)cGRZR& zS1>kwP!ZbtIj!W^oz{1r@2b8VeUJK{wmqS&{qE^I)^~s3aldo??jhH&?`reZI+fGP z7gnpc7Uxw~B~Oi(n-#CRv-^&r`NX>s+f5|aMBlQRz0c73@5LK!sYk=2jqYmr&9i6m za$c&s*y(8A^0eItC5j#`dUyZdx4zx0yt$Y}AKz0xKX;zTyy&{myR3aymrM;e)wzCc zrd6{{`fA4}!3~jlUv_`3lJ{gzxQhAj!G5z9+ zrf@x{l^pz{3xir@xA3`2SO&G&x>~H{u^tCK%2lI0d($hS9Y+qM;n zC#({$Y`eUEmH4FVVQ*%CcY7Znmv@Jm<2%!Y{2j~>8fVl@yqvZ5CCh9BjmvxvIZcy0 zCTeVyId^29Pl(Y+mc8mm8_k8;8|KcuF{P7PGNrhF3eU|`htks>Pc{o@ADA^uT%4TBQt1HRv za_WI0cXwtM*01ol7FgV_cWpvxXnDE0dhY#y$@@j*)2O;=AAt8MZ;~=LqTaR zvo}kg8&B3KRD4@sc&P2T<}ZPzFE%8u=AE7MV)Me8MLvtZ1ztOOj#K*ErNHV-9q0Z2 z%&y<19WiTP;*7ao^Mh7bIhY^yd>XMec=N)g?~>mB-M;GgG3oUha^Z!ELE*;cTV98M z4&}aiKl7%)HG}sK=7bj0PfT;=c&nJX-E?b{T_^kVboZ3478h2^7MpxERViHURPgff zqesFv2EP&H@4h1_=;*U8Qb0FpqQ>$GdmnKI>KkZCM9iJZe2vqOqy2DVx#YQPVwnYH zcP~9l*`T>C>v)v?-JD5EU;l~Tz5C;1^(&V@%yq?cclB5o>}nLedT1Z}q~ZgGS6*E| zJ(-hX`P@V+|AHH*A5Q!?_iGwo^h>*2#~03gx2Atd?k;PKIY$zk$}D{zG9R%|pHW

%WtHmrwt4kb6^+@w8X3JyNB4`LJV3RmS-~g<~e2DavB38x1-He2zW)*Za}mZ2!yK_m^fq zt=awfUwKJkVTHmx_VYOtCU)GnUt5#BYDyCMGJs zukd;Om$jZzb88OAan8J;(7*NNSGE%-DOHD$JaO~k?)tX$*8KXY8-DX=UiLbrw?nJy_^)?YLXO#1U0!$J(0bj@eJR(M z8GkarS}OZO+vi#1n}ly(1`XV+p44dH4c=|Li)Yp!%PmjVUv8F_%lVoxnLSyqr((m2 zW_KIrz+X1c8N}44{+v5G|NoD(*Y7`yum4f>elCB0)aGC2{cF7XJV9;PBy+^$X4=ux}1;kS^M?w~4(bY_1>wuRSr^uTv&fvrdkDp1SE}oFf0> z${Q2geJfv_KI|g*?OCG#z5TbfmmK+;&d36$aZ|m$?QSH)O^hbE{jftP%_Ljc#{5tu3+`OInhvn~AneH^3 zG4;*VgzNiyKbBcHoP22VnTF#8xi->g#5Z>yf86b| z*+wj>!-e(HrYFiz9(UHfIeUCwP0!QhY`eD47bo>IzYKo$+b6>z{rPW=$yOyNw2uh# zoaH-jSEBIb=6?CQ*UA6W%H3bDO5Iv~OD?MJW_7;p-?(ea&z^SY=V{LRd*Ie>b-u$7 z+K#`Ec|6;&@|KC^q?xvFx{gjP<-fb{R$%GT)T_$J;wH>4{+P;pYxb$>76)SvJZ)yy zdLnc2p~0-PRntl>jV)xwJEhK6nF#J(V{B}e>$_#wuk#m;`LCV#tBbJyJnf3w#MA7X zytnQtD>)%q#5QSm;h z@HY}CTB?33&Gt(_;&{w4GEUn4?j2RDuL=L`cUiWa^SiXBZ%JM7ZuK+`U!TujYb~BF zk2}}*a<i>oVuX$S%E0_&ued(F${@htDCh^b= z&gC;V+-aG_DXW&^dn5bMci$cNTjr|8l$-fZN^lgKdt|Ay-LelV_ajB#*-G!5b!fHb zos2^lEVytAPO8q7!s#_V)TH{g>_x{jNy*bYgd5QNGCH8U0>ZQb1TxzW^ zepn~|uIOR1Ud;PLyMJk()?9kHbq}|A?fXOLV}9)8jxT<=c}iX@ceF;PmHWQ=k2g=b zYjrAbBKPv|n;*wkTxpfRpg6zu;bXm+r-#zSlY07QI=Alnpdy=P|Ma5Q@s(wXg09vv z67BKI5dnnK94_6oO?g<-r2*jAQuCULyo0GQUSMkU6 zRLSQvKF^t4wtZT`xpkY{Uac`Jm;8Ids^FQNg0t}im0X)!a+|Wv%l=wd7k{ZdFCTXQ z=GQqBlK;M0x>vb|U36J~_Q7lWl*)YO-{1V$$Y^!`h4*?^6!R#~SzunYD6_TWdUu4LpRly6k=GF!!C0kDN_F~=0tM{_3io%M@A@&rt$g~9 zIF(+dPm8?I2x}jGskFUexj*mcSw9+omq>I64>URhCj@PLxGfZOrejfH$Q8C(m?F3I|Aj;Jjz-cVV# z%lMy){gzv7bJp7%UP^XY|5jp)Z*%-#m%l$!bAsPj%Nk$!vaCw}UaS79r#^O*7oIzG z|Lfd0)#2SQ=2$h)Nju)a-G7R6_qm*{l3%Klwr!N#elBm@?zL6M1rs$Y-hY1}E0^;z zVKMviih@1t-ZnEHJI-d84np^m*?MqvNam-ddhYjq%gk|8K9<>hD5 z$`rG>Z~mxGxlH<_q4GN8M_kI=BsN~`xcnk~f{?P8TZx2VPsOE2!4J8bQpQ5_rigK2?=*Si^Otnjyq+l7eUU25&5RKNpYmWOL zyR3MSV3QoC`{Cd2-rGCF{&xCaw&z&-|Ixz##S@CPg0Fwq+W5ux(2?+od#b3IE73nL3Ieb17_oXlj$y!4z@ z1p|mHoSZ;M3h0`e80%S@m>L@@=o%Xs>lqmtnVBgV8XFkunVXth7%EtrTk2U_7?_yH zLbSPM=A;@afEUmqtqZioSX&M9I7A1?*%0q5fWs$~zO$!GNWQzLi*I6qg1%=;YFk|!)6pW3H^$ZL^0c2>R5N&E+RV^E&%)T!!UVyBE0|i7_#>R5# zyM!y48h~OPdJu*s(u!Gp>pzW6%#fFV#!fn)*KELZ?0sdI%ys9f0#3J?ow5==WWKp4 z*D}nKXftsVxx}1rFX*iOmf_<0jdR~mKKp6a3ZM6yze^UcI(T`T`K^N{_vSKHHLo*Q z5eoT!YQ4z6tPGvR%FrvHpCqpEv7ThIbV{w`b^m*3zdT)YT>1^i?*gfqe5+qYXJ5-E ze!Y7uUgAK-DbHzvyt;`$qm%E?X4K$L{q5EKaqs-^*Z0(=)tnbw!6CCy$~0?6&f){6 z%a|t>ovU=ayh-PDPs7Fk?-p-;b8Xf9W368-7Hd9lN^cZWY5dYv!YNX?gvnE3szMZvZT(VxufA*v0hrK->TWVQ6)E468J;Bj@TvwGv zmF>L5k)lc$#Ux{arWaaqwF??q-g@lxjO$~zo1!xN(^HGCs|;Ro$2P1voqX)oI~fW2 zGOaVEGc)oZY%D9=;(hhgUq%bLEnhIpRa5k{T0mt2DE~rB2~hE1Kt!?PnwOlP0xAXI zg@>*OtXxe=%qsyITdWWbF3o~sx%5NwL-R6|^HWk4z)1wTzymFyhL-d`sd?!o8HNgG zhGw9$70&`<6GJlt z3_BPO+}M=!b<(Bd+paQnUS@oIhA&JSS-H7<%f4*tXIjE?;K;S73x7|l zJN(D^z!s*AsB8Bmu0#vPv7flj;5Ci?{p$qt&GQ*{a5+Q>#@w?>%>DS6;j@{;yf+!s zoi9Fq!FEnl~&-cYn-VxGUv3Ch`!j(M@s`D-~@i8?-@GY>A zVx1u0>t^4#KHM$sP2bV|emhDhE_|Z>p-QyQ`PX^%DbHvAUY-(DnO9N$DCjPG!>+}G zg@5?%w^mG2nP{0JKl#X=Q!)u>6LzMb2+LqND=a1Mkh4+!x85{^w7qSfmx_9nV)pC{ zJUKryo{{sX#T!--~|i7x6o$x8*_ElZAY1 zGn(bS`d?jGu;GlR?GL73kE{-|+!4Pnxg;qwCf$(NwQ%n4bMDPPJ>9RPH1?XX=SK9T zt#J`h+_F$sJD^`7%;8vnXiVQ?CN2ZkpEH`A&b0KnnIyB$3Xx~rDX`Ccnt{|>zOxwz zwRD^XwEs)r_u8_MHFs4i@2ra~jJ~VV1(>8R_VKMb=5=+os3OOV7B`=;>^WUEKCKOr z8Sd6m8+gtN1T>bdacQ@TJ8i+rBoum^|G0L;f@|xq1TI~7zOmtEM5CJzqe*`Dy<^O? z^p+%VwYpca$T;EPUWbs%@)&csx=;Uanx85Sd}R9WB8^!d+&|L_W$ed2jv_PdvoyN>kSOkl1%?G{xT*VV->U4ZONH+7y0e7$tmJ<^g{nLavZ7PbiV^Y}mq*%pt$qHR;YzRMo zw`KCTiH{FId0=qRz;jM{D@XW=aMQ^@yytoT5uZ2v=l+U0e-<}@nZ6lv<=A{iYLhS$W33={EUgee#@H|dltWSzb<>X_RE97xnDV6 zN>pSu@NH;g-1ODeDPE;byz^eciZ>T7C~ljax0Gk;@t2vWp2_N1pRg%ykW1*3x2SD6 zc{_gF{F{Hdi)!w_tUa@5uWj|rJ$v6)$KRE{d;0Ay%Zcl6dfxhdVs-73^TzY)*Oc?* zzj#`G{p+_cZq=s2+dXg3IG4L*@$${>H*Y!jeV_9#@mX%%mYuc6zbB}ldVkNOZkOVG z{*$SDVkV#6zFy;Dcm>x_pQNvSrICBB#9v?C6lfPP`K@!Ga+Zd>6P zo)(cVVYK0`!e<97t?;m5(~KGEBp17##OU`T=UlLiH+;$ zPniGHJnv@Hr;k^+EV$>+{P^$HEi3Q2tGd2i^F8qKkAFN7-L6-$|pCo0_MOaH$&Ov8fA(0 zLS@S{8rxQ_JabgdSaPvgeo*k#EUx9L-H}mueU}~yoiH)aQ*~F!qRq><@7QL)cXx5z z_pF~Ar#PN|D*S%#`ggVA@8%tt6TAJ@g>752ch}3js(Fxi`JVYbo3QU+F8>y@dv}wu z>Gopr9UEuaJ^dc6buBaZ@ukg^8g<$W=Y(1ROlv=xVbu0VE&JB5ood-qGqVi@7avPW z%C*U#`gc#C?}WcrWjd!~{CY%k&PH$f;I};Z?vc`?+E0bM!_J)A_i`FfwPWDJEjj9% zB`Z&T{eAJ(C3S(kH|F#A$#30fGErhz`8WIhDTVKirOof`w*BdP`l;~#-+w-?^$*(g z@yYM?dy{^BTR8tz?CxD*_x2|GR=;W#UVpj$yWayJ=7b6C1>0AoXSz+Cl6h%{q?^Bq z%2}p!M^4u5J?S!OOH`6;MuX#u~Y40ioS~8WbL_2gk%i`tgFB8dl;7X zSo^=o%~L+!oi=ePD$_k8pO^-1n`v8AdhYy<>aE#J+mfQ7?0dR*{)Eg4e`n5IbJR%f$@UCUqv#S9XZMu5Y_h_Szn-&x-xIic zi_5&vRZeG*zFKghOJeQy8zPtAye!*xB6PyR-u{?>f4+Jjn)rzO_BWf|mEW3EP5QU; z=WkeB_3%sTr)xJgzS&Ry_wJyq^mm_~tv{>WQ_>DkPq-lUzMCgyp<7X(dFJHF#~3`L z!scY0+LE&FQ?yQwlhdj<<|^M+j5}vNVUg87QMWX0Y25Tn%*v*#_w-F!yNyvf%}`A_ z#%Y#bbl}y^*H9_6VmiM*{gpkR`@ZVg7e|3DE6dk$d+vzb^un^2jg!;E z&(SH#>BRgMPp1^`FMGl@o%Lt1r_y9Ssk2AoGM-QRpL*c`r^-n-bB@>@=5_6JIW+O! zWRI_&LY80`~L3}@zdV5w17)R z=k14A^Y?#T{XAS=?r^G1a!}j5-Q{0*g&qz!&v{d+P?XmFDUH`fzaY(5{#b(2Gw)!v z8KqI#Mhi?g_q|EkeNgW=FWbEXOE$4F&z_ez`R~hD(uqBNM`nNDT`lvrWg@SXyY=-s z)4Y!8xZPd)b61##>Kx0WpR3dB-`(AiN%bq;**`;~gF!0Q#&tcssGdz4fy?idb z-XhhP%2CAP#guo-ie<98#;&6Yp?Q4|wrvZ0_H2i%e*>3eV-?es(_*Vvg?R7;DJooe zqafOJ{UDcthfV7ZoS;slc`zg!ygR*L*(+PmUaap>IB zVd|Pw7v0Z0e^&6yswr8l=3nnzmMUY{DlYb@4@zo{xHxCV?B;(*mT+xU(llo;nRMf# zzuHux&_uPJ>0gX5EZ4}?d|kjkJHvPF`Pj_OxBu?6+?=^8V&$xWg>6f2ajgn==F0VY zc|+yPa~b}8Ig_Nq12gZan6bY95NP(|{8`S;?+Sh-W))pJnWQ>*&okldyuRHtpLI;l zdlj-M?0WCBmHVbm3g|to!S!&xgQvruiid7067F009)|~noC*mDdcIjZ>2t?C2a7j? zto{}Pe0*-!;@wSwJm1yT+pX^~HZD5m?pa{)>yn!E*PL&`$qS~Q`EY#uyOpb#Wp3rJ znNnEMa!nysc1r1#&TlWxc4uB${_Oos$5PRd)do4+wM}xQrfjezOp{Kzk8d((@nl<3nN6j zK2Nd8yC~aH|2}i;rE^D}^7z&k9g|Aj*Y7p8X|~tFADugYFMqbvwcNnLbHUmmTc26# zWdYq0c2&E)BHwMRn!ihBUr&^l22=0HP?Pg(?`a>D-n$#!a40R zGuY~vKAIEwaBo?g^PCpygc5*eUHl0sfK571>1@T`@e^1>e+*|U=a`r8mx~<{Q zey@6c^wll4`(G*)4V#7fig)Z6cKdQODRIRy$FF>QA0+3jnP0@GeAR-5|EWw;CFfI_ zkmnbI=N|UhY8aAv=abMu_Z*3j%O;<{c(Q5tPNR3r&*XEg3cR&)QI^WAu$e(ZD^_f} z`S;t5C(jI~6mxt$67u+h_L8vP84>LcmhJP5`nt44gLZ7u^40%wB<-U9Hm;CWA2q9k zUj#<>OABWk0_W+$?ad`|8(3 z)g2Q29H%VOf;Dt?e9doODQ_%V=;|LJ($l0~w(#)ZNlRj$U6b?@C@c$(So3}AihEZ} z_p#?>Pl-tjnf&VK)18)oc~1Ryy%c*}f8x7eZ1)Y76-v&3=QU_q$uZ^kZ}my&U20zi@rSe>cWU&Gk?ST5~Ax{njh@*zPY(St2mo>et_YMXp7~cdmtPjQSZ_$nER3 zc+J=CZOnB~Hz_t7@tv~ZP)*X*P@eWp*lo6t@X=>Qvs9*OSIlghezah1)9WKeUPdSG zPb&{vwx~^LDYMv*H;X26&CI`b=venD&!2O5S|&dJdFMaV#Ho9yV~&$pg2pQIld`zr zJH|mH8i)Z{$bc+l{MpG71cvDNvn6Qw1$j6Iw7DL`;}Gv6ysrQbAC&QD9HB5^!AhjTFnoiRMn)FMLrt*(2eS?v@a+AqU6R9p)N^;T0nhXX{^;X- z>Kc-@4_-KNtdQ^L@5J?|6WBd%=FO|9idnT+rxn*pT z|5^BIUb?X4^jE94t3FojGH1Ct zT>oCHFEQ!&ar@B2JJxU4XF4^ZH0IW-Ur$dretPll+&WFgr?cEjL-tf0vfUt&6gc%4 z+qV^kotW9dg4AI{9I1iWVM9o2)MHW&zttd)O<1$dNG~+UW0|hfvQ)5$wGzGXA z=*VP{v_c+K%+S;nbYcjKn1O+jg@FP{$N;X+z`z)Et_esGE@o_O44QU95i>9_24w{_ zF%!_~A}C_U7N%w<77#e-_4==2;kFG0y~xc_vugXNJW*Q!M70V=>Phi+Pq< z%(K8W&(Z+XJWB&i^DK=p&9gMbVxBP;^Ng{WXNtu<6D;PLVKL7Pi+L7U%rnPgo+TFZ zEHTV8GQi9qMg|5L<{24a<_{wSBP`}&<_{wS%=}?wfSEsx3^4PDkpX7@Ffzc*A4Ue4 z`NPNnGk+KvV&)Gc1I+wkWN3;}UKtr$V3Zq1Mg|6EXl1dHk&%HZnwYVX0qC?Xl=8*c z$jBVME;BZ=1f86Os?HcxQ-XvL<)pDWXcit#%)$V45EMudUUnK=7$KMKB}IvuIjKdU zmU3`rRVt`T)(4%A1e!($U%~)hO8}Y}236n1C5c5P;L6+B!rah+OI6j?-;E0ZZJf_s literal 0 HcmV?d00001