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>
40 KiB
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
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
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:
- Volume GUID (
GetVolumeNameForVolumeMountPoint) — primär, überlebt Buchstabenwechsel. - Volume Serial + Filesystem + Capacity — GUID fehlt (manche exFAT/USB).
- Serial + Label + Capacity — schwächer.
- Device Instance ID (USB PnP) als Zusatzsignal.
- 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_keybleibt UUID; UNC istlast_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
- Job anlegen, Status Scanning,
scan_generation++. - Exclude-Regeln laden.
- Iterative DFS/Post-Order mit
System.IO.Enumeration.FileSystemEnumerableund\\?\-Präfix. - Parallelität: lokal 4 Enum-Worker, SMB/NFS 1–2.
- Pro Batch (2 000–5 000 Einträge) eine Transaktion an den SQLite-Writer.
- Ordnergrößen post-order im Scan akkumulieren (Stack), nicht per Parent-Walk pro Datei.
- Einzeldateifehler →
scan_errors, weiter. - Access Denied → loggen, Ordner überspringen.
CancellationTokenauf jedem Worker; nach Cancel konsistenter Stand (abgeschlossene Batches committed).- 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=Presentnur 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
FSCTL_QUERY_USN_JOURNAL→UsnJournalID,NextUsn.- Persistiert in
sources.usn_journal_id/usn_next. - Beim Start / periodisch / nach Watcher-Burst:
FSCTL_READ_USN_JOURNALab gespeichertem USN. - Records (
USN_RECORD_V2/V3) aufIndexCommandmappen:- Create → Insert
- Delete → Tombstone/Delete
- Rename old/new → path_rel umschreiben
- Data/Basic info → Size, Times, Attributes
- FRN (
FileReferenceNumber) →entries.file_id. - Nach erfolgreichem Batch
usn_nextfortschreiben 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 /
FindFirstFileinkrementeller 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:
- Kinder enumerieren.
- Vergleich mit
entriesdiesesparent_id. - Neu → insert, fehlend → tombstone, Mtime/Size geändert → update.
- 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(Prefixabc*) *.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-Suchepath_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):
TabFokus anderes PanelF5/F6Refresh (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>
→ 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 viaLargestDirectoriesAsync(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), Sortierungaggregate_size/size_bytesDESC. - Actions: Open (aktuelles Pane / anderes Split-Pane / neuer Tab), Search within, Rescan (Indexer-Queue), Copy path.
- Virtualisierte
ListView, Children-CapAnalysisTreeChildTake.
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.
- SQL:
GROUP BY size_bytes HAVING COUNT(*) > 1(nur Present, nicht excluded). - Gruppen in
hash_queue, große Dateien niedrige Priorität oder umgekehrt je nach User-Ziel. - Partial Hash: erste 64 KiB SHA-256 (
hash_state=1). - Nur kollidierende Partial-Gruppen: Full-File SHA-256,
content_hashpersistieren. - Worker:
SetThreadPriorityBackground, 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_pathupdaten, 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:
- Tombstones auf
entries(status=Deleted,deleted_utc) — Datei existierte. Retention-Policy (sofort / N Tage / dauerhaft). - Source-Rollups täglich: eine Zeile in
source_stats_history(Größe, Counts). Das liefert „+384 GB seit letztem Monat“ ohne Dateihistorie. - 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<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 (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:
- Produktname und AppId (Arbeitsname Explorer ist ungeeignet für Store/Mutex/
LocalAppData). - Hybrid Live-FS + Index vs. Index-first Listing (Hybrid).
- Elevation: nie Admin / optional USN-Dienst / App immer elevatet (USN opportunistisch, kein Pflicht-Admin).
- Split-Default: neues Tab single oder immer dual (single, Split per Tab).
- Tombstones default: sofort löschen / 30 Tage / dauerhaft (30 Tage).
- Erstes Laufwerk: Banner „Index aufbauen“ vs. stiller Scan (Banner, User startet Scan).
- Sprache UI: de / en / beide (de + en, System-UI).
- Distribution: portable ZIP / MSIX / Setup (MSIX oder Setup, DB unter LocalAppData).
- Mindest-OS: Windows 10 22H2 vs. 11 only (Windows 10 1809+ / 11).
- WPF-Fluent-Kit: WPF-UI (lepoco) vs. eigene ResourceDictionaries (WPF-UI für Tempo, austauschbar hinter Themes).
- Copy-Engine: nur IFileOperation vs. CopyFileEx-Queue (Mischung, siehe §11).
- 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:
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.- CsWin32 — not used. Win32 is called via
LibraryImport/DllImportinExplorer.Windows. CsWin32 generated code was heavier than needed for the small API surface (volumes, USN,CopyFileEx, Recycle Bin). - WPF-UI (lepoco) — not used. Light/Dark Fluent-style brushes live in
Themes/Dark.xamlandThemes/Light.xamlso the UI toolkit stays replaceable. PRAGMA mmap_size/ largecache_size— not applied at runtime. They made SQLite native startup unreliable under concurrent test hosts; WAL +synchronous=NORMALremain.- App data folder —
%LocalAppData%\ExplorerWorkbench(notExplorer) so the working name does not collide with Windows Explorer. - Background work — in-process
IHostedServiceinstances (indexer, transfer queue, hash worker, history rollup, watchers). No Windows Service in this run. - Search syntax — structured
SearchQueryexists; Everything-like lexer is not shipped (Phase 7). - UNIQUE identity —
UNIQUE (source_id, ifnull(parent_id,-1), name_norm)because SQLite UNIQUE treats NULLs as distinct. - INSERT ids —
Microsoft.Data.Sqlite+ DapperExecuteScalarAsynconINSERT … RETURNINGleaves the write connection busy and hangs the next command. Writer-connection SQL usesSqliteCommand(SqliteExec) andlast_insert_rowid(). - SQLite cache —
SqliteCacheMode.Shareddeadlocked reader+writer connections in-process. Connections use the default private cache. - Schema apply — Microsoft.Data.Sqlite/
Executesplitting on;brokeCREATE TRIGGERbodies. Schema is applied as an explicit statement array (SchemaScript.Statements). - Search CurrentFolder —
SearchRequest.DirectChildrenOnlyrestricts to the folder’s children in SQL (not a client-side filter after paging). - Duplicate hashing — full-file hashes run only when another same-size file already shares the partial hash. Unique partial hashes skip the full read.