Files
Explorer-Workbench/docs/ARCHITECTURE.md
netquick e9aba73552 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>
2026-08-22 12:43:05 +02:00

40 KiB
Raw Blame History

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:

  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_JOURNALUsnJournalID, 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 identityUNIQUE (source_id, ifnull(parent_id,-1), name_norm) because SQLite UNIQUE treats NULLs as distinct.
  9. INSERT idsMicrosoft.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 cacheSqliteCacheMode.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 CurrentFolderSearchRequest.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.