# Explorer — Technischer Entwurf Arbeitsname: **Explorer**. Mentales Modell für den Benutzer: > Explorer öffnen → Dateien sehen → Dateien verwalten → sofort suchen → Speicher analysieren. Die Datenbank, der Index und „Sources“ sind interne Konzepte. In der UI heißen sie Laufwerke, Ordner, Offline-Medien und Suche — niemals Catalog, Collection oder Database. Dieses Dokument ist die Grundlage vor der Implementierung. Es trifft Empfehlungen inklusive Trade-offs. --- ## 1. Empfohlene Gesamtarchitektur ### 1.1 Stil **Modularer Monolith** mit klaren Projektgrenzen, nicht Microservices. - Eine Solution, mehrere Assemblies. - Die Indexierungs- und Speicherlogik kennt **kein WPF**. - Die WPF-App ist ein Client der Application-Schicht. - Später können CLI, Windows-Dienst oder ein Web-UI dieselbe Application-Schicht nutzen. **MVVM ist Pflicht** für die UI (`CommunityToolkit.Mvvm`). Views enthalten kein Business; ViewModels orchestrieren Use-Cases; Domain/Indexer bleiben UI-frei. ### 1.2 Schichten ``` ┌─────────────────────────────────────────────────────────────┐ │ Explorer.App WPF Shell, Themes, Views, Templates │ ├─────────────────────────────────────────────────────────────┤ │ Explorer.Presentation Navigation, Split, Dialoge, VM │ ├─────────────────────────────────────────────────────────────┤ │ Explorer.Application Use-Cases, Jobs, Fortschritt, Policy │ ├───────────────┬──────────────┬──────────────┬───────────────┤ │ Indexing │ FileOps │ Search │ Analysis │ │ ChangeTrack │ TransferQ │ Query │ Duplicates │ │ SourceMgmt │ │ │ History │ ├───────────────┴──────────────┴──────────────┴───────────────┤ │ Explorer.Domain Entities, Value Objects, Policies │ ├───────────────────────────────┬─────────────────────────────┤ │ Explorer.Storage.Sqlite │ Explorer.Windows (Win32) │ │ Schema, FTS5, Bulk, WAL │ USN, Volumes, Recycle, IO │ └───────────────────────────────┴─────────────────────────────┘ ``` Abhängigkeitsregel: **innen kennt außen nicht**. `Explorer.Domain` hat keine SQLite-, Win32- oder WPF-Referenz. `Explorer.Indexing` hängt an Domain + Abstraktionen. Konkrete SQLite- und Win32-Adapter liegen außen. ### 1.3 Laufzeitmodell (V1) Alles läuft im Desktopprozess, aber **nicht** auf dem UI-Thread: | Worker | Verantwortung | |---|---| | UI / Dispatcher | Rendering, Input, Bindings | | SQLite-Writer (1 Thread) | Alle Schreibzugriffe, Transaktionen | | SQLite-Reader Pool | Suche, Ordnergrößen, Analyse | | Scan Workers | Enumeration, begrenzt parallel | | USN / Watcher | Change intake → Index-Commands | | Transfer Workers | Copy/Move/Delete Queue | | Hash Workers | Duplikate, niedrige I/O-Priorität | SQLite erlaubt nur einen Writer. Deshalb ein **Command-Kanal** (`System.Threading.Channels`) in den Writer-Thread. Leser nutzen WAL und blockieren die UI nicht. ### 1.4 Live-FS vs. Index (zentrale UX-Entscheidung) **Empfehlung: Hybrid.** | Situation | Datenquelle | |---|---| | Aktueller Ordner, Source online | Live-Dateisystem, virtuelle Liste | | Ordnergrößen, Suche, Analyse, Offline | Index | | Gerade erstellte Datei im offenen Ordner | sofort sichtbar (Live-FS), Index holt nach | Nur-Index-UI fühlt sich nach Katalog an und hinkt hinter dem Explorer her. Nur-FS-UI verschenkt den Index. Der Hybrid hält das Explorer-Gefühl und nutzt das Gedächtnis dort, wo es den Nutzen stiftet. ### 1.5 Warum nicht WinUI 3 / MAUI Der Auftrag ist WPF. WPF hat die reifste Desktop-Splitter-/Virtualisierungs-Geschichte und reicht für Fluent-Optik (Themes, optional WPF-UI). WinUI 3 bleibt eine spätere Shell-Option, weil die Core-Projekte UI-frei sind. --- ## 2. Projekt-/Solution-Struktur ``` Explorer.sln ├── src/ │ ├── Explorer.App/ # WPF, app.manifest, Themes │ ├── Explorer.Presentation/ # ViewModels, UI-Services │ ├── Explorer.Application/ # Use-Cases, Job-Orchestrierung │ ├── Explorer.Domain/ # Source, Entry, Policies │ ├── Explorer.Indexing/ # Scan, USN-Apply, Watcher-Apply │ ├── Explorer.Search/ # Query-Modell, FTS-Adapter │ ├── Explorer.Analysis/ # Folder sizes, Top-N, Duplikate │ ├── Explorer.FileOperations/ # Transfer Queue, Konflikte │ ├── Explorer.Storage.Sqlite/ # Schema, Repositories, FTS5 │ ├── Explorer.Windows/ # CsWin32: USN, Volume, IFileOperation │ └── Explorer.Contracts/ # DTOs / später IPC ├── tests/ │ ├── Explorer.Domain.Tests/ │ ├── Explorer.Indexing.Tests/ │ ├── Explorer.Storage.Tests/ │ └── Explorer.FileOperations.Tests/ └── docs/ └── ARCHITECTURE.md ``` **Trade-off:** Mehr Projekte als ein einziges `Explorer.Core` bedeuten etwas mehr Ceremony, aber verhindern, dass WPF-Types in den Indexer sickern. Das ist die Wartbarkeits-Priorität. Zielframework: **.NET 10 LTS** (Stand 2026). WPF, `net10.0-windows`, Windows 10 1809+ / Windows 11. NuGet-Kern (V1): | Paket | Zweck | |---|---| | CommunityToolkit.Mvvm | MVVM, Messenger, AsyncRelayCommand | | Microsoft.Data.Sqlite | SQLite | | Dapper | Dünnes Mapping, keine EF-Hotpath | | Microsoft.Windows.CsWin32 | USN, Volumes, Recycle Bin | | Serilog | Datei-Log, Scan-Fehler | | Microsoft.Extensions.Hosting | DI, BackgroundService im Prozess | **Kein Entity Framework** für den Index. EF ist ungeeignet für Millionen Bulk-UPSERTs, FTS5 und partielle USN-Updates. --- ## 3. Kernkomponenten und Verantwortlichkeiten | Komponente | Verantwortung | Nicht verantwortlich | |---|---|---| | **Shell / Navigation** | Fenster, Tabs, Tree, Breadcrumb, History, Themes | Index, Transfers | | **ExplorerPane** | Ein navigierbares Panel (Pfad, View-Mode, Selection, Sort) | Anderes Panel, DB | | **SplitHost** | 1 oder 2 Panes, Splitter, DnD-Ziel | Datei-I/O selbst | | **Source Manager** | Entdecken, identifizieren, Online/Offline, Anzeige-Name | Scanning | | **Indexer** | Full scan, incremental, Resume, Excludes, Reparse-Policy | UI, File copy | | **Change Tracker** | USN lesen, Watcher-Events, zu Index-Commands normalisieren | Persistenz-Details | | **Storage** | Schema, Transaktionen, FTS, Aggregates | Win32 | | **Search Engine** | Filter/Query → SQL/FTS, Ranking | Index aktualisieren | | **Analysis Engine** | Top-Dirs/Files, Typen, Drilldown aus Aggregates | Live-Walk des FS | | **Duplicate Detector** | Size → Partial Hash → Full Hash, I/O-Throttle | UI-Layout | | **File Operations** | Queue, Progress, Recycle, Clipboard, Konflikte | Index-Schreiben (Indexer beobachtet Ergebnis) | | **Platform** | Volumes, USN, Long Paths, Icons, Recycle, Share-Detect | Business-Regeln | Kommunikation intern über: - Synchron: Application-Services (aktuelle Ordnerliste, Suche). - Asynchron: `IProgress`, `Channel`, Messenger für „Source status changed“. Indexer-Events aktualisieren den Index; der ExplorerPane subscribed auf „current folder invalidated“ und refresht nur den offenen Pfad. --- ## 4. Datenmodell / SQLite-Schema ### 4.1 Betriebsmodus ```sql PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON; PRAGMA temp_store = MEMORY; PRAGMA mmap_size = 268435456; -- 256 MB, tunen PRAGMA cache_size = -131072; -- 128 MB PRAGMA busy_timeout = 5000; ``` Schema-Version über `PRAGMA user_version`. Migrationen als eingebettete SQL-Skripte. ### 4.2 Tabellen ```sql CREATE TABLE sources ( id INTEGER PRIMARY KEY, stable_key TEXT NOT NULL UNIQUE, -- App-UUID, nie Buchstabe kind TEXT NOT NULL, -- NtfsLocal, Removable, Smb, Nfs, Cloud display_name TEXT NOT NULL, -- "SanDisk Ultra 64 GB" volume_guid TEXT, -- \\?\Volume{...}\ volume_serial INTEGER, -- NTFS/FAT serial filesystem TEXT, -- NTFS, exFAT, SMBFS, NFS label TEXT, capacity_bytes INTEGER, device_instance_id TEXT, -- USB PnP, optional last_root_path TEXT, -- E:\ oder \\media\movies status TEXT NOT NULL, -- Online, Offline, Scanning, Stale, Error last_seen_utc TEXT, last_indexed_utc TEXT, usn_journal_id INTEGER, -- 64-bit als INTEGER usn_next INTEGER, scan_generation INTEGER NOT NULL DEFAULT 0, last_error TEXT ); CREATE TABLE entries ( id INTEGER PRIMARY KEY, source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE, parent_id INTEGER REFERENCES entries(id), name TEXT NOT NULL, -- Original-Schreibweise name_norm TEXT NOT NULL, -- Unicode-Fold für Suche/Unique extension TEXT, -- ohne Punkt, lower is_dir INTEGER NOT NULL, size_bytes INTEGER NOT NULL DEFAULT 0, -- Datei: logische Größe aggregate_size INTEGER NOT NULL DEFAULT 0, -- Dir: Summe Kinder+selbst child_file_count INTEGER NOT NULL DEFAULT 0, child_dir_count INTEGER NOT NULL DEFAULT 0, created_utc TEXT, modified_utc TEXT, last_seen_utc TEXT NOT NULL, last_indexed_utc TEXT, attributes INTEGER NOT NULL DEFAULT 0, file_id INTEGER, -- NTFS FRN parent_file_id INTEGER, reparse_tag INTEGER, -- 0 = keines status INTEGER NOT NULL DEFAULT 0, -- 0 Present, 1 Offline, 2 Deleted, 3 Unknown deleted_utc TEXT, path_rel TEXT NOT NULL, -- relativ zum Source-Root, \ getrennt content_hash BLOB, -- SHA-256, nullable hash_state INTEGER NOT NULL DEFAULT 0, -- 0 none, 1 partial, 2 full UNIQUE (source_id, parent_id, name_norm) ); CREATE INDEX ix_entries_parent ON entries(source_id, parent_id, status); CREATE INDEX ix_entries_ext_size ON entries(source_id, extension, size_bytes) WHERE is_dir = 0; CREATE INDEX ix_entries_size ON entries(source_id, size_bytes) WHERE is_dir = 0 AND status = 0; CREATE INDEX ix_entries_modified ON entries(source_id, modified_utc); CREATE INDEX ix_entries_file_id ON entries(source_id, file_id) WHERE file_id IS NOT NULL; CREATE INDEX ix_entries_path ON entries(source_id, path_rel); CREATE INDEX ix_entries_status ON entries(source_id, status); CREATE VIRTUAL TABLE entries_fts USING fts5( name, name_norm, extension, content = 'entries', content_rowid = 'id', tokenize = 'unicode61' ); CREATE TABLE excludes ( id INTEGER PRIMARY KEY, scope TEXT, -- global oder source_id source_id INTEGER REFERENCES sources(id), kind TEXT NOT NULL, -- PathPrefix, Glob, Extension, Attribute pattern TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE scan_jobs ( id INTEGER PRIMARY KEY, source_id INTEGER NOT NULL, kind TEXT NOT NULL, -- Full, Incremental, Folder, Rebuild status TEXT NOT NULL, -- Queued, Running, Cancelled, Failed, Done started_utc TEXT, finished_utc TEXT, files_seen INTEGER NOT NULL DEFAULT 0, dirs_seen INTEGER NOT NULL DEFAULT 0, bytes_seen INTEGER NOT NULL DEFAULT 0, resume_path TEXT, -- letzter abgeschlossener Ordner error_count INTEGER NOT NULL DEFAULT 0, last_error TEXT ); CREATE TABLE scan_errors ( id INTEGER PRIMARY KEY, job_id INTEGER NOT NULL, path TEXT, kind TEXT, -- AccessDenied, Invalid, Io, Network message TEXT, utc TEXT NOT NULL ); CREATE TABLE transfer_jobs ( id INTEGER PRIMARY KEY, op TEXT NOT NULL, -- Copy, Move, Delete, Rename src TEXT NOT NULL, dst TEXT, status TEXT NOT NULL, bytes_total INTEGER, bytes_done INTEGER, created_utc TEXT NOT NULL, error TEXT ); CREATE TABLE hash_queue ( entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE, size_bytes INTEGER NOT NULL, priority INTEGER NOT NULL DEFAULT 0, state TEXT NOT NULL -- Pending, PartialDone, Done, Error ); CREATE TABLE source_stats_history ( id INTEGER PRIMARY KEY, source_id INTEGER NOT NULL, captured_utc TEXT NOT NULL, total_size INTEGER NOT NULL, file_count INTEGER NOT NULL, dir_count INTEGER NOT NULL ); CREATE TABLE directory_stats_history ( id INTEGER PRIMARY KEY, source_id INTEGER NOT NULL, path_rel TEXT NOT NULL, -- nicht jede Datei, nur Dirs über Schwellwert captured_utc TEXT NOT NULL, aggregate_size INTEGER NOT NULL, file_count INTEGER NOT NULL ); CREATE INDEX ix_dir_hist ON directory_stats_history(source_id, path_rel, captured_utc); ``` ### 4.3 Pfade speichern oder rekonstruieren? **Empfehlung: `path_rel` denormalisiert speichern.** - Rekonstruktion über Parent-Kette ist bei 10M Zeilen und Suche zu teuer. - Speicher: 10M × ~80 Byte ≈ 0,8 GB — akzeptabel neben dem Nutzen. - Rename eines Vorfahren: `UPDATE entries SET path_rel = $new || substr(path_rel, length($old)+1) WHERE source_id=? AND path_rel LIKE $old || '\%' ESCAPE ...` SQLite `NOCASE` ist **nur ASCII**. Deshalb `name_norm` in der App (Unicode-Fold) und FTS5 `unicode61`. Niemals auf `COLLATE NOCASE` für deutsche/asiatische Namen verlassen. Root-Eintrag jeder Source: `parent_id IS NULL`, `path_rel = ''`. Anzeige-Pfad = `sources.last_root_path` + `path_rel`. Offline: Anzeige über `display_name` + `path_rel`. --- ## 5. Strategie für Volume-Identifikation Eine Source hat eine **stabile App-UUID** (`stable_key`). Laufwerksbuchstaben sind nur `last_root_path`. ### 5.1 Lokale / Wechselmedien Fingerprint, Match-Reihenfolge: 1. **Volume GUID** (`GetVolumeNameForVolumeMountPoint`) — primär, überlebt Buchstabenwechsel. 2. **Volume Serial + Filesystem + Capacity** — GUID fehlt (manche exFAT/USB). 3. **Serial + Label + Capacity** — schwächer. 4. **Device Instance ID** (USB PnP) als Zusatzsignal. 5. Unklar → Benutzer: „Ist das derselbe Datenträger wie SanDisk Ultra 64 GB?“ Nicht als Identität verwenden: nur Buchstabe, nur Label, nur Größe. ### 5.2 SMB - Kanonische Form: `\\server\share` (Hostname lower-case, Share original). - Zusätzlich speichern: eingegebener Pfad, ggf. IP. - `stable_key` bleibt UUID; UNC ist `last_root_path`. - Hostname vs. IP vs. FQDN können dieselbe Share sein — V1 nicht automatisch mergen; manuell „als dasselbe Medium verbinden“ reicht. ### 5.3 NFS Windows-NFS-Client (optional Feature) erscheint als Laufwerk oder Mount. Behandeln wie Netzwerk-FS ohne USN. Identität: Server+Export-Pfad. ### 5.4 Cloud (Plugin-Overlay) Explorer **implementiert keine Synchronisation**. Offizielle Clients (OneDrive, Google Drive, Nextcloud; später Dropbox) bleiben zuständig für Sync, Hydration, Konflikte und Anmeldung. Cloud-Ordner werden wie jedes andere gemountete Dateisystem über Win32 gelistet. `Explorer.Plugin.Abstractions` / `IStorageProvider` **reichern** Einträge an (Status, logische vs. allokierte Größe, Pin/Free-up), sie **ersetzen nicht** Enumeration, Watcher oder Source-Identität. - Fehlendes, deaktiviertes oder fehlerhaftes Plugin: normales Browse. - Index, Thumbnails, Duplikat-Hash und Analyse **dürfen online-only Dateien nicht hydrieren**. Lesen von Dateiinhalten nur bei expliziter Nutzeraktion (Öffnen, Kopieren, Pin). - Fähigkeiten sind entdeckbar (`CloudState`, `Pin`, `Dehydrate`, `Quota`, …), nicht hart verdrahtet. - Verträge sind IPC-tauglich (`InProcess` / `OutOfProcess`). PluginHost/Marketplace sind nicht Teil dieses Schritts. ### 5.5 Offline-Anzeige Tree-Knoten: ``` SanDisk Ultra 64 GB Offline · Zuletzt gesehen 18.08.2026 ``` Statusfarbe/Badge. Doppelklick öffnet den Index-Browser (read-only Listing + Suche), keine Live-Operationen außer „Pfad kopieren“ / „merken, wohin kopiert werden sollte“. --- ## 6. Initialscan-Strategie Manuell pro Source. Kein stiller Vollscan aller Festplatten beim ersten Start — das wäre unexplorer-typisch und I/O-feindlich. Beim ersten Öffnen eines Laufwerks: dezentes Banner „Index anlegen, damit Suche und Ordnergrößen funktionieren“ mit Aktion **Index aufbauen**. ### 6.1 Algorithmus 1. Job anlegen, Status Scanning, `scan_generation++`. 2. Exclude-Regeln laden. 3. Iterative DFS/Post-Order mit `System.IO.Enumeration.FileSystemEnumerable` und `\\?\`-Präfix. 4. Parallelität: lokal 4 Enum-Worker, SMB/NFS **1–2**. 5. Pro Batch (2 000–5 000 Einträge) eine Transaktion an den SQLite-Writer. 6. Ordnergrößen **post-order im Scan** akkumulieren (Stack), nicht per Parent-Walk pro Datei. 7. Einzeldateifehler → `scan_errors`, weiter. 8. Access Denied → loggen, Ordner überspringen. 9. `CancellationToken` auf jedem Worker; nach Cancel konsistenter Stand (abgeschlossene Batches committed). 10. Fortschritt: Dateien, Ordner, Bytes, aktueller Pfad, Fehlerzahl — UI via `IProgress`, gedrosselt (~10 Hz). Thread-/Prozess-I/O-Priorität: `PROCESS_MODE_BACKGROUND_BEGIN` während Scan, damit Explorer und andere Apps fluide bleiben. ### 6.2 Resume (V1 pragmatisch, V1.1 vollständig) - V1: unterbrochener Scan gilt als unvollständig; **erneuter Full Scan mit UPSERT** (idempotent). `status=Present` nur für gesehene Einträge der neuen Generation; Rest → Deleted gemäß Retention. - V1.1: `resume_path` = letzter **abgeschlossener** Ordner; Skip bereits geschriebener Subtrees. UPSERT-Schlüssel: `(source_id, parent_id, name_norm)`. FRN zusätzlich für Rename-Erkennung. ### 6.3 Millionen Dateien Grobe Rechnung: 5M Dateien × ~250 Byte Indexzeile ≈ 1,25 GB plus FTS und Indizes → **2–4 GB DB** realistisch. SSD lokal: Initialscan CPU/I/O-gebunden, nicht SQLite-gebunden, sofern Batches und ein Writer stimmen. --- ## 7. NTFS-USN-Journal-Strategie ### 7.1 Ziel Nach dem Initialscan **kein** periodischer Full Scan auf lokalen NTFS-Volumes. Änderungen seit `usn_next` einlesen und anwenden. ### 7.2 Ablauf 1. `FSCTL_QUERY_USN_JOURNAL` → `UsnJournalID`, `NextUsn`. 2. Persistiert in `sources.usn_journal_id` / `usn_next`. 3. Beim Start / periodisch / nach Watcher-Burst: `FSCTL_READ_USN_JOURNAL` ab gespeichertem USN. 4. Records (`USN_RECORD_V2/V3`) auf `IndexCommand` mappen: - Create → Insert - Delete → Tombstone/Delete - Rename old/new → path_rel umschreiben - Data/Basic info → Size, Times, Attributes 5. FRN (`FileReferenceNumber`) → `entries.file_id`. 6. Nach erfolgreichem Batch `usn_next` fortschreiben **in derselben Transaktion**. Journal-ID geändert oder USN zu alt (Journal wrapped): Source → **Stale**, Banner „Index veraltet — aktualisieren“, kein stilles Vollscan ohne Auftrag (außer User-Setting „automatisch neu aufbauen“). ### 7.3 Rechte — wichtiger Trade-off Das USN-Journal ist oft nur mit **Administrator** oder Backup-Privilegien vollständig lesbar. | Option | Vorteil | Nachteil | |---|---|---| | A Immer als Admin | Zuverlässiges USN | UAC, schlechte Desktop-UX | | B Optionaler Index-Dienst als SYSTEM | USN ohne UI-Elevation | Komplex, V2 | | C **V1: USN wenn möglich, sonst Fallback** | Kein Pflicht-Admin | Manche Volumes nur Watcher+Walk | **Empfehlung C für V1**, Architektur für B offen halten (`IChangeFeed`). Fallback lokal ohne USN: - `FileSystemWatcher` (best effort, Buffer können überlaufen). - Directory-mtime / `FindFirstFile` inkrementeller Ordnerwalk beim Öffnen und per manuellem Refresh. - Watcher-Overflow → Source **Stale**. FileSystemWatcher **nie** als alleinige Wahrheitsquelle. ### 7.4 Coalescing USN liefert viele Reasons plus oft `USN_REASON_CLOSE`. Apply-Logik coalesct pro FRN im Batch (letzte Create/Delete/Rename gewinnt), um Schreiblast zu senken. --- ## 8. Strategie für SMB/NFS Kein USN, unzuverlässige Watcher, hohe Latenz. **Empfehlung: Hybrid aus opportunistischem Walk und manueller Aktualisierung.** | Trigger | Aktion | |---|---| | Ordner öffnen (online) | Live-Listing; Diff gegen Index für diesen Ordner (Namen, Size, Mtime) | | Manuell Refresh | Ordner oder Source | | Optional Timer (User) | z. B. alle 30 min, nur gemountete Shares, idle | | Watcher, falls der Redirector Events liefert | best effort, Overflow → Stale | Inkrementeller Ordner-Rescan: 1. Kinder enumerieren. 2. Vergleich mit `entries` dieses `parent_id`. 3. Neu → insert, fehlend → tombstone, Mtime/Size geändert → update. 4. Rekursiv nur wenn User „Ordner neu einlesen“ oder Full Rescan. Netzwerk-I/O: **ein Enum-Worker pro Share**, Timeouts, Reconnect-Backoff. Verschwundene Share → Offline, Index bleibt. NFS unter Windows nur, wenn der optional Client installiert ist. Sonst UNC/NFS nicht erzwingen; Source-Typ `Nfs` vorsehen, Provider kann „nicht verfügbar“ liefern. --- ## 9. Suche und benötigte DB-Indizes ### 9.1 Syntax vs. UI-Filter **V1: UI-Filter + schnelles Suchfeld für Namen/Glob.** **V1.1: kompakte Query-Syntax.** Begründung: Syntax ohne Parser-Disziplin wird zur Falle (`size > 10 GB` vs. `size>10GB`). Everything-Nutzer erwarten sie später. Die Query-Engine sollte intern schon ein strukturiertes `SearchQuery`-Objekt haben, das die Syntax später nur parst. V1 Suchfeld: - Freitext → FTS5 auf `name` / `name_norm` (Prefix `abc*`) - `*.mkv` → Extension-Filter - optional ein Filter-Panel: Typ, Größe von–bis, Datum, nur Ordner, Source-Scope V1.1 Syntax (Everything-ähnlich, klein halten): ``` *.mkv size:>10gb type:video size:>5gb modified:<2025-01-01 *.zip size:>1gb path:Downloads ``` Keine vollständige Programmiersprache. Ein PEG/Regex-Lexer reicht. ### 9.2 Scopes `SearchScope`: CurrentFolder | CurrentTree | Selected | Sources[] | AllKnown (inkl. Offline). CurrentFolder kann Live-FS filtern (sofort). CurrentTree und AllKnown **nur Index** — das ist der Produktvorteil. ### 9.3 Indizes (siehe Schema) Hot Paths: - FTS5 für Teilnamen - `(source_id, parent_id, status)` Listing - `(source_id, extension, size_bytes)` Typ+Größe - `(source_id, size_bytes)` Duplikat-Kandidaten - `(source_id, path_rel)` Tree-Suche `path_rel LIKE 'Movies\%'` - `(source_id, file_id)` USN Query-Plan in Tests mit `EXPLAIN QUERY PLAN` gegen eine 1M-Zeilen-Fixture absichern. Ergebnis-Limit default 10 000, virtuelle Liste, „mehr laden“. Niemals 2M Rows in die UI binden. --- ## 10. Architektur der Split View Tabs und Split sind **orthogonal**. ``` Window └── TabStrip mehrere Arbeitsbereiche └── ExplorerTab └── SplitHost Single | Dual (vertikal, später horizontal) ├── ExplorerPane A eigene History, Pfad, View, Selection └── ExplorerPane B ``` Nicht: Tabs ersetzen Split. Nicht: ein globaler Dual-Pane ohne Tabs (Total Commander). Beides: - Tab = Arbeitskontext (z. B. „Videos sortieren“). - Split in einem Tab = zwei unabhängige `ExplorerPane`. Default neues Tab: **Single**. Aktion „Split“ öffnet rechtes Panel mit demselben Pfad oder dem zweiten selektierten Ordner. Layout persistieren pro Tab. Jedes Pane: - `NavigationService` (Back/Forward/Up, Breadcrumb) - `FolderViewModel` (Items, Sort, Filter, ViewMode Details/List/Icons) - `IDropTarget` / `IDragSource` DnD und Copy/Paste zwischen Panes = dieselben `FileOperations` wie innerhalb eines Panes. Quelle und Ziel sind Pfade, nicht „Panel-IDs“. Tastatur (V1 anlehnen, später konfigurierbar): - `Tab` Fokus anderes Panel - `F5` / `F6` Refresh (nicht TC-Copy, um Explorer-Nutzer nicht zu verwirren) - Copy/Paste Standard, plus später TC-Shortcuts als Option Tree links: **fensterweit**, nicht pro Pane — sonst wird die UI eng. Klick setzt das **aktive** Pane. Optional später Tree pro Pane. --- ## 11. Dateioperationen und Transfer Queue ### 11.1 V1-Operationen Öffnen, Kopieren, Verschieben, Umbenennen, Löschen (Recycle), Neuer Ordner, DnD, Clipboard (`CF_HDROP` / Shell IDLists), Pfad kopieren. Asynchron, UI nie blockieren. Kurze Ops (Rename, New Folder) ohne Queue-Fenster; lange Ops in die Queue. ### 11.2 Queue ``` UI → FileOperationService.Enqueue(op) → Channel → 1–2 Worker (lokal parallel begrenzt, SMB serieller) → Progress (bytes_done) → TransferPanel ``` V1: Queue, Fortschritt, Abbrechen, Fehler anzeigen. Später: Pause/Resume (`CopyFileEx` COPY_FILE_RESTARTABLE), Konfliktdialog, Retry, Undo (Rename/Move inner volume). ### 11.3 Windows-Integration | Op | API | |---|---| | Recycle Delete | `IFileOperation` (empfohlen) oder `SHFileOperation` mit `FOF_ALLOWUNDO` | | Copy/Move mit Progress | `CopyFileEx` / `MoveFileWithProgress` **oder** `IFileOperation` mit Progress-Sink | | Open | `ShellExecute` / `Process.Start(UseShellExecute=true)` | | Icons | `SHGetFileInfo` / ImageList, gecacht | **Empfehlung V1:** `IFileOperation` für Delete-to-Recycle (korrekte Undo, Elevation-Prompt). Eigene Copy-Engine (`CopyFileEx`) für Queue-Kontrolle. Trade-off: zwei Pfade vs. volle Kontrolle über Pause und Throughput. Locked Files: Fehler in der Queue, Rest weiter. USB während Copy entfernt: Job Failed, Source Offline. Nach erfolgreicher Op: Indexer per Command (oder USN) aktualisieren — nicht auf Watcher hoffen. Long Paths: Manifest `longPathAware`, intern `\\?\` / `\\?\UNC\`. --- ## 12. Analyse- / Folder-Size-Konzept Der Index hält `aggregate_size` und Child-Counts **immer aktuell**, nicht beim Öffnen der Analyse. ### 12.1 Pflege der Aggregates - **Initialscan:** Post-Order, einmal schreiben. - **USN/incremental:** Delta an der Datei entlang der Parent-Kette (`parent_id`) addieren/subtrahieren. Tiefe selten > 20; bei Rename: subtract am alten Ast, add am neuen. - Niemals `SUM(*)` über den ganzen Tree für die Explorer-Spalte. ### 12.2 Explorer-Spalte Optionale Spalte „Größe“ für Ordner aus `aggregate_size`. Wenn Index Stale/Scanning: graue Zahl + Hinweis. Online ohne Index: Spalte leer oder „—“, kein heimlicher Recursive Walk (sonst sind wir wieder WinDirStat). ### 12.3 Analyse-View Eigenes Overlay, nicht den Explorer ersetzen. Default ist ein **hierarchischer Tree** der indexierten Sources und Ordner (WinDirStat-ähnlich, Explorer-Workbench-Look). Ranking-Views daneben: Biggest folders, Biggest files, By file type, By source. - Tree: Roots = Sources (`GetRootAsync`); Expand lädt Kinder lazy via `LargestDirectoriesAsync(sourceId, parentId)` aus dem Index. Nie den kompletten Tree in WPF materialisieren, nie das Dateisystem walken. - Balken im Tree relativ zu den **Geschwistern des aktuellen Parents**; in Ranking-Views relativ zur Liste. - Biggest folders/files zeigen vollständigen oder gekürzten Pfad (`PathRules.ShortenDisplay`), Sortierung `aggregate_size` / `size_bytes` DESC. - Actions: Open (aktuelles Pane / anderes Split-Pane / neuer Tab), Search within, Rescan (Indexer-Queue), Copy path. - Virtualisierte `ListView`, Children-Cap `AnalysisTreeChildTake`. V1 kein Treemap (Canvas/WPF-Heavy). Datenmodell erlaubt Treemap später (`aggregate_size` pro Kind). WinDirStat-Vorteil: **O(log n) Queries statt Disk-Walk**. Nachteil: Zahl so gut wie der Index — deshalb Status „aktuell / veraltet“ sichtbar machen. --- ## 13. Duplikaterkennung Nicht jede Datei hashen. 1. SQL: `GROUP BY size_bytes HAVING COUNT(*) > 1` (nur Present, nicht excluded). 2. Gruppen in `hash_queue`, große Dateien niedrige Priorität oder umgekehrt je nach User-Ziel. 3. Partial Hash: erste 64 KiB SHA-256 (`hash_state=1`). 4. Nur kollidierende Partial-Gruppen: Full-File SHA-256, `content_hash` persistieren. 5. Worker: `SetThreadPriority` Background, max. 1 sequentieller Reader pro Volume, pausieren wenn Transfer-Queue aktiv. Filter: Source, Pfad-Prefix, Exclude-Liste. Identisch = gleicher Full Hash, nicht gleicher Name. Hardlinks (gleiche FRN): als „gleiche Datei, mehrere Pfade“ markieren, nicht als Duplikat-Müll. V1 der Detector kann nach dem Explorer-MVP kommen; Schema (`content_hash`, `hash_queue`) trotzdem in V1 anlegen, damit kein zweites Migration-Chaos entsteht. --- ## 14. Offline-Media-Konzept - Source bleibt im Tree, Badge **Offline**, `last_seen_utc`, Kapazität/Label. - Entries: `status=Offline` (Volume weg) vs. `Deleted` (USN/Scan hat Löschung gesehen). - Navigation: Index-Listing, Suche über Offline-Medien **an**. - Dateioperationen: Copy/Move/Delete disabled; Öffnen disabled; Pfad kopieren erlaubt. - Wieder da: Fingerprint-Match, `last_root_path` updaten, USN oder Folder-Diff, Status Online. USB mitten im Scan: Job abbrechen/fehlschlagen, committed Batches behalten, Source Offline, Banner „Scan unvollständig“. --- ## 15. Historienkonzept (speichereffizient) Nicht jede Datei versionieren. **Drei Stufen:** 1. **Tombstones** auf `entries` (`status=Deleted`, `deleted_utc`) — Datei existierte. Retention-Policy (sofort / N Tage / dauerhaft). 2. **Source-Rollups** täglich: eine Zeile in `source_stats_history` (Größe, Counts). Das liefert „+384 GB seit letztem Monat“ ohne Dateihistorie. 3. **Directory-Rollups** nur für Ordner über Schwellwert (z. B. > 1 GB oder Top 200 je Source) in `directory_stats_history`. Retention: 90 Tage täglich, danach wöchentlich verdichten. Kein Copy-on-Write des ganzen Trees. Kein Git fürs Dateisystem. Spätere UI: Delta-Badge am Ordner, wenn zwei Snapshots existieren. V1 nur Schema + nächtlicher Rollup-Job. --- ## 16. Fehler- und Recovery-Konzept Prinzip: **Fehler sind lokal, Jobs sind unterbrechbar, die DB überlebt Crashes.** | Ereignis | Verhalten | |---|---| | Access Denied | `scan_errors`, weiter | | Invalid/corrupt dirent | loggen, weiter | | Netzwerk weg | Source Offline, Job Failed/Cancelled, Index behalten | | USB gezogen | wie Netzwerk; offene Transfers Failed | | Locked file (Hash/Copy) | Eintrag/Job Error, Queue weiter | | Sehr lange Pfade | `\\?\`, sonst Error-Log | | Unicode | UTF-8 in SQLite, Originalname in `name` | | UI-Crash während Scan | WAL; letzte Transaktion committed; Scan-Job beim Start als unterbrochen markieren | | SQLite I/O error | Writer stoppt, Banner, keine stillen Writes | | Cancel | keine halben Batches; `usn_next` nur nach Commit | | Scan während Änderungen | USN nach Scan-Ende nachziehen; oder generation-mark + missing → Deleted | | Journal lost | Stale + Full Rescan nötig | DB-Integrität: nach Crash `PRAGMA quick_check` beim Start (asynchron, UI nicht blockieren). Backup optional: periodisches `VACUUM INTO` später, nicht V1. Logging: Serilog rolling file unter `%LocalAppData%\…\logs`. Scan-Fehler zusätzlich in DB für die UI-Liste „übersprungene Ordner“. --- ## 17. Performance-Risiken | Risiko | Mitigation | |---|---| | UI-Freeze durch Binding von 100k Items | Virtualizing `ListView`/`DataGrid`, Paging, nie ganze Source materialisieren | | SQLite Writer-Stau | Ein Writer, große Batches, keine UI-Reads im Writer | | FTS5 bloat | External content table, `optimize` nach Full Scan | | Watcher-Flood | Coalesce 300–500 ms, Overflow → Stale | | SMB-Latenz | Live-Listing async, Placeholder-Zeilen, Timeout | | Recursive LIKE ohne Index | Immer `source_id` zuerst; Prefix-LIKE nur mit Index auf `path_rel` | | Aggregate-Update-Ketten bei Massen-Delete | Batch-Delta pro Parent statt N einzelne UPDATEs wo möglich | | RAM | Streaming Enum, kein `List` der ganzen Platte; DB mmap statt alles in CLR | | Hash I/O | Throttle, ein Disk-Head, pause on user copy | | Icon extraction | Async, Default-Icon zuerst, Cache | | TreeView alle Drives expand | Lazy, nur sichtbare Children aus FS oder Index | Ziel-SLO (Richtwerte, kein Contract): - Ordner mit 10k Dateien öffnen (online, lokal): < 100 ms bis erste Zeilen. - Namenssuche 5M Rows, indexed: < 200 ms für erste Seite. - UI während Scan: Eingabe ohne Ruckeln; Scan-Progress 10 Hz. --- ## 18. MVP-Abgrenzung ### MVP (fühlbarer Explorer + Gedächtnis) - WPF Shell, Dark/Light - Nav-Tree: lokale Volumes + manuell UNC - Ein ExplorerPane: Breadcrumb, Back/Forward/Up, Details+List - Tabs - Split View + DnD zwischen Panes - Live-FS Listing wenn online - Basis-Ops: Open, Copy, Move, Rename, Delete→Recycle, New Folder, Clipboard, Pfad kopieren - Transfer-Queue mit Progress + Cancel - SQLite-Index, manuelle Source-Auswahl, Full Scan async - Excludes (Pfad, Glob, Ext, Hidden/System optional) - Reparse: nicht folgen (siehe unten) - Ordnergröße aus Index (Spalte) - Suche: Name/Glob/Größe/Datum, Scope aktueller Tree oder alle Sources inkl. Offline - Offline-Volume im Tree - Source-Status: Online / Scanning / Stale / Offline / Error - Manuell: Refresh, Rescan Folder, Full Rescan ### Nicht im MVP - USN (sofort danach Phase 2) - Windows-Dienst - NFS-Feinschliff (Typ vorsehen, nur wenn Client da) - Query-Syntax - Duplikat-UI (Schema ja) - Historien-Charts (Schema ja) - Pause/Resume Transfers, Konflikt-UI, Undo - Icon-View, Treemap, Preview-Pane - Weitere Cloud-Provider (Dropbox) und PluginHost/IPC - Explorer.exe ersetzen - Volltext in Dateiinhalten ### Reparse-Policy (V1 festlegen) | Typ | Default | |---|---| | Directory Junction / Directory Symlink | **Nicht folgen**; als Link-Eintrag indexieren, Overlay-Icon | | Volume Mount Point | Als **eigene Source** behandeln, wenn Volume-GUID bekannt; sonst nicht in den Baum des Parents mergen | | File Symlink | Als Datei indexieren, Größe des Links; Hash nicht dem Target folgen | | Andere Reparse (OneDrive placeholder, Dedup) | Als Datei/Ordner mit Tag; Enumeration normal, wenn Win32 sie als Datei zeigt | Schleifen: besuchte `(volume_guid, FRN)`-Menge während eines Scans. Zweites Betreten → skip. --- ## 19. Entwicklungsphasen ### Phase 0 — Gerüst (ca. 1 Woche) Solution, DI, Logging, leeres Fenster, Themes, SQLite-Ping, Volume-Enumeration ohne Index. ### Phase 1 — Live-Explorer (2–3 Wochen) Pane, Tree, Breadcrumb, History, Details-View, Virtualization, Basis-Ops ohne Queue, Recycle, Long Paths. **Fühlt sich schon wie Explorer an.** ### Phase 2 — Tabs, Split, Queue (2 Wochen) Tabs, Dual Pane, DnD, Transfer-Queue, Progress-Panel. ### Phase 3 — Index-MVP (3 Wochen) Schema, Source-Identität, Full Scan, Excludes, Aggregates, Offline, Ordnergrößen-Spalte, Suche über Index, Refresh/Rescan. ### Phase 4 — Change Tracking (2 Wochen) USN wo möglich, Watcher+Folder-Diff Fallback, Stale-Logik, Resume-Scan. ### Phase 5 — Analyse (1–2 Wochen) Analyse-View, Balken, Drilldown, Typ-Aggregation. ### Phase 6 — Duplikate + Historie (2 Wochen) Hash-Pipeline, Duplikat-UI, Rollups, Tombstone-Retention-Settings. ### Phase 7 — Härten SMB-Stabilität, Elevation-optional, Query-Syntax, Dienst-Schnittstelle (`Explorer.Contracts` IPC), Installer. Phasen 1–3 sind das kleinste lieferbare Produkt, das die Vision trägt. USN ist bewusst Phase 4: ohne soliden Scan und Identity ist das Journal wertlos. --- ## 20. Entscheidungen vor Implementierungsbeginn Bitte diese Punkte klären. Empfohlene Defaults in Klammern: 1. **Produktname und AppId** (Arbeitsname Explorer ist ungeeignet für Store/Mutex/`LocalAppData`). 2. **Hybrid Live-FS + Index** vs. Index-first Listing (**Hybrid**). 3. **Elevation:** nie Admin / optional USN-Dienst / App immer elevatet (**USN opportunistisch, kein Pflicht-Admin**). 4. **Split-Default:** neues Tab single oder immer dual (**single, Split per Tab**). 5. **Tombstones default:** sofort löschen / 30 Tage / dauerhaft (**30 Tage**). 6. **Erstes Laufwerk:** Banner „Index aufbauen“ vs. stiller Scan (**Banner, User startet Scan**). 7. **Sprache UI:** de / en / beide (**de + en, System-UI**). 8. **Distribution:** portable ZIP / MSIX / Setup (**MSIX oder Setup, DB unter LocalAppData**). 9. **Mindest-OS:** Windows 10 22H2 vs. 11 only (**Windows 10 1809+ / 11**). 10. **WPF-Fluent-Kit:** WPF-UI (lepoco) vs. eigene ResourceDictionaries (**WPF-UI für Tempo, austauschbar hinter Themes**). 11. **Copy-Engine:** nur IFileOperation vs. CopyFileEx-Queue (**Mischung, siehe §11**). 12. **Netzwerk-Shares im Tree:** nur manuell vs. zuletzt verwendete vs. Windows-Netzwerkumgebung (**manuell + Recents**). --- ## Hintergrunddienst: A vs. B | | A In-Process (V1) | B Windows Service (später) | |---|---|---| | Komplexität | niedrig | Session 0, ACL, IPC, Updates | | Index wenn UI zu | stoppt | läuft weiter | | USN-Rechte | oft unzureichend | SYSTEM kann Journal lesen | | Crash-Isolation | UI-Crash stoppt Index | getrennt | | Empfohlen | **V1 = A** | **V2 = B**, wenn Identity+Schema stabil sind | V1 so schneiden, dass der Indexer ein `IHostedService` ist. Im Dienst-Host später derselbe Service, UI spricht über Named Pipe / gRPC (`Explorer.Contracts`). Nicht in V1 bauen, Interfaces nicht an WPF kleben. --- ## Sicherheit und Robustheit (kurz) - Keine Indexierung von Inhalten geschützter Ordner ohne User-Exclude-Defaults: `C:\Windows`, `C:\System Volume Information`, `$Recycle.Bin`, `C:\ProgramData\Microsoft`, Standard-Temp. - Scans laufen in User-Rechten; nicht heimlich SeBackupPrivilege fordern. - DB liegt im User-Profil, nicht world-writable. - UNC-Pfade nicht als ausführbare Kommandos interpretieren. - Hash-Worker lesen nur; keine Writes ins FS. --- ## UX-Sprache (verbindlich) | Intern | UI | |---|---| | Source | Laufwerk, Medium, Speicherort | | Entry | Datei, Ordner | | Full Scan | Index aufbauen / Vollständig einlesen | | Stale | Möglicherweise nicht aktuell | | Tombstone | Früher vorhanden / Nicht mehr vorhanden | | FTS / SQLite | — (nie zeigen) | | USN | — (nie zeigen; höchstens „Änderungsprotokoll des Laufwerks“) | Statuszeile darf „1,2 Mio. Dateien indexiert · aktuell“ sagen. Das ist Explorer-Gedächtnis, kein Datenbankprodukt. --- ## Implementation notes (this codebase) Deviations from the design above, with reasons: 1. **`entries.scan_generation`** — added so a full/folder scan can mark unseen rows as deleted without keeping the whole tree in RAM. UPSERT stamps the generation; missing rows with an older generation become tombstones. 2. **CsWin32** — not used. Win32 is called via `LibraryImport`/`DllImport` in `Explorer.Windows`. CsWin32 generated code was heavier than needed for the small API surface (volumes, USN, `CopyFileEx`, Recycle Bin). 3. **WPF-UI (lepoco)** — not used. Light/Dark Fluent-style brushes live in `Themes/Dark.xaml` and `Themes/Light.xaml` so the UI toolkit stays replaceable. 4. **`PRAGMA mmap_size` / large `cache_size`** — not applied at runtime. They made SQLite native startup unreliable under concurrent test hosts; WAL + `synchronous=NORMAL` remain. 5. **App data folder** — `%LocalAppData%\ExplorerWorkbench` (not `Explorer`) so the working name does not collide with Windows Explorer. 6. **Background work** — in-process `IHostedService` instances (indexer, transfer queue, hash worker, history rollup, watchers). No Windows Service in this run. 7. **Search syntax** — structured `SearchQuery` exists; Everything-like lexer is not shipped (Phase 7). 8. **UNIQUE identity** — `UNIQUE (source_id, ifnull(parent_id,-1), name_norm)` because SQLite UNIQUE treats NULLs as distinct. 9. **INSERT ids** — `Microsoft.Data.Sqlite` + Dapper `ExecuteScalarAsync` on `INSERT … RETURNING` leaves the write connection busy and hangs the next command. Writer-connection SQL uses `SqliteCommand` (`SqliteExec`) and `last_insert_rowid()`. 10. **SQLite cache** — `SqliteCacheMode.Shared` deadlocked reader+writer connections in-process. Connections use the default private cache. 11. **Schema apply** — Microsoft.Data.Sqlite/`Execute` splitting on `;` broke `CREATE TRIGGER` bodies. Schema is applied as an explicit statement array (`SchemaScript.Statements`). 12. **Search CurrentFolder** — `SearchRequest.DirectChildrenOnly` restricts to the folder’s children in SQL (not a client-side filter after paging). 13. **Duplicate hashing** — full-file hashes run only when another same-size file already shares the partial hash. Unique partial hashes skip the full read.