Add Explorer Workbench with hierarchical, off-UI Storage analysis.

Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

897
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,897 @@
# 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<ScanProgress>`, `Channel<IndexCommand>`, 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, später Google Drive / 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 **12**.
5. Pro Batch (20005000 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 → **24 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 vonbis, 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 10000, 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<TransferJob>
→ 12 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 300500 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<File>` 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 (Google Drive, 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 (23 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 (12 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 13 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 folders 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.