908 lines
43 KiB
Markdown
908 lines
43 KiB
Markdown
# 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, Google Drive, Nextcloud; später Dropbox) bleiben zuständig für Sync, Hydration, Konflikte und Anmeldung.
|
||
|
||
Cloud-Ordner werden wie jedes andere gemountete Dateisystem über Win32 gelistet. `Explorer.Plugin.Abstractions` / `IStorageProvider` **reichern** Einträge an (Status, logische vs. allokierte Größe, Pin/Free-up), sie **ersetzen nicht** Enumeration, Watcher oder Source-Identität.
|
||
|
||
- Fehlendes, deaktiviertes oder fehlerhaftes Plugin: normales Browse.
|
||
- Index, Thumbnails, Duplikat-Hash und Analyse **dürfen online-only Dateien nicht hydrieren**. Lesen von Dateiinhalten nur bei expliziter Nutzeraktion (Öffnen, Kopieren, Pin).
|
||
- Fähigkeiten sind entdeckbar (`CloudState`, `Pin`, `Dehydrate`, `Quota`, …), nicht hart verdrahtet.
|
||
- Verträge sind IPC-tauglich (`InProcess` / `OutOfProcess`). PluginHost/Marketplace sind nicht Teil dieses Schritts.
|
||
|
||
### 5.5 Offline-Anzeige
|
||
|
||
Tree-Knoten:
|
||
|
||
```
|
||
SanDisk Ultra 64 GB
|
||
Offline · Zuletzt gesehen 18.08.2026
|
||
```
|
||
|
||
Statusfarbe/Badge. Doppelklick öffnet den Index-Browser (read-only Listing + Suche), keine Live-Operationen außer „Pfad kopieren“ / „merken, wohin kopiert werden sollte“.
|
||
|
||
---
|
||
|
||
## 6. Initialscan-Strategie
|
||
|
||
Manuell pro Source. Kein stiller Vollscan aller Festplatten beim ersten Start — das wäre unexplorer-typisch und I/O-feindlich. Beim ersten Öffnen eines Laufwerks: dezentes Banner „Index anlegen, damit Suche und Ordnergrößen funktionieren“ mit Aktion **Index aufbauen**.
|
||
|
||
### 6.1 Algorithmus
|
||
|
||
1. Job anlegen, Status Scanning, `scan_generation++`.
|
||
2. Exclude-Regeln laden.
|
||
3. Iterative DFS/Post-Order mit `System.IO.Enumeration.FileSystemEnumerable` und `\\?\`-Präfix.
|
||
4. Parallelität: lokal 4 Enum-Worker, SMB/NFS **1–2**.
|
||
5. Pro Batch (2 000–5 000 Einträge) eine Transaktion an den SQLite-Writer.
|
||
6. Ordnergrößen **post-order im Scan** akkumulieren (Stack), nicht per Parent-Walk pro Datei.
|
||
7. Einzeldateifehler → `scan_errors`, weiter.
|
||
8. Access Denied → loggen, Ordner überspringen.
|
||
9. `CancellationToken` auf jedem Worker; nach Cancel konsistenter Stand (abgeschlossene Batches committed).
|
||
10. Fortschritt: Dateien, Ordner, Bytes, aktueller Pfad, Fehlerzahl — UI via `IProgress`, gedrosselt (~10 Hz).
|
||
|
||
Thread-/Prozess-I/O-Priorität: `PROCESS_MODE_BACKGROUND_BEGIN` während Scan, damit Explorer und andere Apps fluide bleiben.
|
||
|
||
### 6.2 Resume (V1 pragmatisch, V1.1 vollständig)
|
||
|
||
- V1: unterbrochener Scan gilt als unvollständig; **erneuter Full Scan mit UPSERT** (idempotent). `status=Present` nur für gesehene Einträge der neuen Generation; Rest → Deleted gemäß Retention.
|
||
- V1.1: `resume_path` = letzter **abgeschlossener** Ordner; Skip bereits geschriebener Subtrees.
|
||
|
||
UPSERT-Schlüssel: `(source_id, parent_id, name_norm)`. FRN zusätzlich für Rename-Erkennung.
|
||
|
||
### 6.3 Millionen Dateien
|
||
|
||
Grobe Rechnung: 5M Dateien × ~250 Byte Indexzeile ≈ 1,25 GB plus FTS und Indizes → **2–4 GB DB** realistisch. SSD lokal: Initialscan CPU/I/O-gebunden, nicht SQLite-gebunden, sofern Batches und ein Writer stimmen.
|
||
|
||
---
|
||
|
||
## 7. NTFS-USN-Journal-Strategie
|
||
|
||
### 7.1 Ziel
|
||
|
||
Nach dem Initialscan **kein** periodischer Full Scan auf lokalen NTFS-Volumes. Änderungen seit `usn_next` einlesen und anwenden.
|
||
|
||
### 7.2 Ablauf
|
||
|
||
1. `FSCTL_QUERY_USN_JOURNAL` → `UsnJournalID`, `NextUsn`.
|
||
2. Persistiert in `sources.usn_journal_id` / `usn_next`.
|
||
3. Beim Start / periodisch / nach Watcher-Burst: `FSCTL_READ_USN_JOURNAL` ab gespeichertem USN.
|
||
4. Records (`USN_RECORD_V2/V3`) auf `IndexCommand` mappen:
|
||
- Create → Insert
|
||
- Delete → Tombstone/Delete
|
||
- Rename old/new → path_rel umschreiben
|
||
- Data/Basic info → Size, Times, Attributes
|
||
5. FRN (`FileReferenceNumber`) → `entries.file_id`.
|
||
6. Nach erfolgreichem Batch `usn_next` fortschreiben **in derselben Transaktion**.
|
||
|
||
Journal-ID geändert oder USN zu alt (Journal wrapped): Source → **Stale**, Banner „Index veraltet — aktualisieren“, kein stilles Vollscan ohne Auftrag (außer User-Setting „automatisch neu aufbauen“).
|
||
|
||
### 7.3 Rechte — wichtiger Trade-off
|
||
|
||
Das USN-Journal ist oft nur mit **Administrator** oder Backup-Privilegien vollständig lesbar.
|
||
|
||
| Option | Vorteil | Nachteil |
|
||
|---|---|---|
|
||
| A Immer als Admin | Zuverlässiges USN | UAC, schlechte Desktop-UX |
|
||
| B Optionaler Index-Dienst als SYSTEM | USN ohne UI-Elevation | Komplex, V2 |
|
||
| C **V1: USN wenn möglich, sonst Fallback** | Kein Pflicht-Admin | Manche Volumes nur Watcher+Walk |
|
||
|
||
**Empfehlung C für V1**, Architektur für B offen halten (`IChangeFeed`).
|
||
|
||
Fallback lokal ohne USN:
|
||
|
||
- `FileSystemWatcher` (best effort, Buffer können überlaufen).
|
||
- Directory-mtime / `FindFirstFile` inkrementeller Ordnerwalk beim Öffnen und per manuellem Refresh.
|
||
- Watcher-Overflow → Source **Stale**.
|
||
|
||
FileSystemWatcher **nie** als alleinige Wahrheitsquelle.
|
||
|
||
### 7.4 Coalescing
|
||
|
||
USN liefert viele Reasons plus oft `USN_REASON_CLOSE`. Apply-Logik coalesct pro FRN im Batch (letzte Create/Delete/Rename gewinnt), um Schreiblast zu senken.
|
||
|
||
---
|
||
|
||
## 8. Strategie für SMB/NFS
|
||
|
||
Kein USN, unzuverlässige Watcher, hohe Latenz.
|
||
|
||
**Empfehlung: Hybrid aus opportunistischem Walk und manueller Aktualisierung.**
|
||
|
||
| Trigger | Aktion |
|
||
|---|---|
|
||
| Ordner öffnen (online) | Live-Listing; Diff gegen Index für diesen Ordner (Namen, Size, Mtime) |
|
||
| Manuell Refresh | Ordner oder Source |
|
||
| Optional Timer (User) | z. B. alle 30 min, nur gemountete Shares, idle |
|
||
| Watcher, falls der Redirector Events liefert | best effort, Overflow → Stale |
|
||
|
||
Inkrementeller Ordner-Rescan:
|
||
|
||
1. Kinder enumerieren.
|
||
2. Vergleich mit `entries` dieses `parent_id`.
|
||
3. Neu → insert, fehlend → tombstone, Mtime/Size geändert → update.
|
||
4. Rekursiv nur wenn User „Ordner neu einlesen“ oder Full Rescan.
|
||
|
||
Netzwerk-I/O: **ein Enum-Worker pro Share**, Timeouts, Reconnect-Backoff. Verschwundene Share → Offline, Index bleibt.
|
||
|
||
NFS unter Windows nur, wenn der optional Client installiert ist. Sonst UNC/NFS nicht erzwingen; Source-Typ `Nfs` vorsehen, Provider kann „nicht verfügbar“ liefern.
|
||
|
||
---
|
||
|
||
## 9. Suche und benötigte DB-Indizes
|
||
|
||
### 9.1 Syntax vs. UI-Filter
|
||
|
||
**V1: UI-Filter + schnelles Suchfeld für Namen/Glob.**
|
||
**V1.1: kompakte Query-Syntax.**
|
||
|
||
Begründung: Syntax ohne Parser-Disziplin wird zur Falle (`size > 10 GB` vs. `size>10GB`). Everything-Nutzer erwarten sie später. Die Query-Engine sollte intern schon ein strukturiertes `SearchQuery`-Objekt haben, das die Syntax später nur parst.
|
||
|
||
V1 Suchfeld:
|
||
|
||
- Freitext → FTS5 auf `name` / `name_norm` (Prefix `abc*`)
|
||
- `*.mkv` → Extension-Filter
|
||
- optional ein Filter-Panel: Typ, Größe von–bis, Datum, nur Ordner, Source-Scope
|
||
|
||
V1.1 Syntax (Everything-ähnlich, klein halten):
|
||
|
||
```
|
||
*.mkv size:>10gb
|
||
type:video size:>5gb
|
||
modified:<2025-01-01
|
||
*.zip size:>1gb
|
||
path:Downloads
|
||
```
|
||
|
||
Keine vollständige Programmiersprache. Ein PEG/Regex-Lexer reicht.
|
||
|
||
### 9.2 Scopes
|
||
|
||
`SearchScope`: CurrentFolder | CurrentTree | Selected | Sources[] | AllKnown (inkl. Offline).
|
||
|
||
CurrentFolder kann Live-FS filtern (sofort). CurrentTree und AllKnown **nur Index** — das ist der Produktvorteil.
|
||
|
||
### 9.3 Indizes (siehe Schema)
|
||
|
||
Hot Paths:
|
||
|
||
- FTS5 für Teilnamen
|
||
- `(source_id, parent_id, status)` Listing
|
||
- `(source_id, extension, size_bytes)` Typ+Größe
|
||
- `(source_id, size_bytes)` Duplikat-Kandidaten
|
||
- `(source_id, path_rel)` Tree-Suche `path_rel LIKE 'Movies\%'`
|
||
- `(source_id, file_id)` USN
|
||
|
||
Query-Plan in Tests mit `EXPLAIN QUERY PLAN` gegen eine 1M-Zeilen-Fixture absichern.
|
||
|
||
Ergebnis-Limit default 10 000, virtuelle Liste, „mehr laden“. Niemals 2M Rows in die UI binden.
|
||
|
||
---
|
||
|
||
## 10. Architektur der Split View
|
||
|
||
Tabs und Split sind **orthogonal**.
|
||
|
||
```
|
||
Window
|
||
└── TabStrip mehrere Arbeitsbereiche
|
||
└── ExplorerTab
|
||
└── SplitHost Single | Dual (vertikal, später horizontal)
|
||
├── ExplorerPane A eigene History, Pfad, View, Selection
|
||
└── ExplorerPane B
|
||
```
|
||
|
||
Nicht: Tabs ersetzen Split. Nicht: ein globaler Dual-Pane ohne Tabs (Total Commander). Beides:
|
||
|
||
- Tab = Arbeitskontext (z. B. „Videos sortieren“).
|
||
- Split in einem Tab = zwei unabhängige `ExplorerPane`.
|
||
|
||
Default neues Tab: **Single**. Aktion „Split“ öffnet rechtes Panel mit demselben Pfad oder dem zweiten selektierten Ordner. Layout persistieren pro Tab.
|
||
|
||
Jedes Pane:
|
||
|
||
- `NavigationService` (Back/Forward/Up, Breadcrumb)
|
||
- `FolderViewModel` (Items, Sort, Filter, ViewMode Details/List/Icons)
|
||
- `IDropTarget` / `IDragSource`
|
||
|
||
DnD und Copy/Paste zwischen Panes = dieselben `FileOperations` wie innerhalb eines Panes. Quelle und Ziel sind Pfade, nicht „Panel-IDs“.
|
||
|
||
Tastatur (V1 anlehnen, später konfigurierbar):
|
||
|
||
- `Tab` Fokus anderes Panel
|
||
- `F5` / `F6` Refresh (nicht TC-Copy, um Explorer-Nutzer nicht zu verwirren)
|
||
- Copy/Paste Standard, plus später TC-Shortcuts als Option
|
||
|
||
Tree links: **fensterweit**, nicht pro Pane — sonst wird die UI eng. Klick setzt das **aktive** Pane. Optional später Tree pro Pane.
|
||
|
||
---
|
||
|
||
## 11. Dateioperationen und Transfer Queue
|
||
|
||
### 11.1 V1-Operationen
|
||
|
||
Öffnen, Kopieren, Verschieben, Umbenennen, Löschen (Recycle), Neuer Ordner, DnD, Clipboard (`CF_HDROP` / Shell IDLists), Pfad kopieren.
|
||
|
||
Asynchron, UI nie blockieren. Kurze Ops (Rename, New Folder) ohne Queue-Fenster; lange Ops in die Queue.
|
||
|
||
### 11.2 Queue
|
||
|
||
```
|
||
UI → FileOperationService.Enqueue(op)
|
||
→ Channel<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 via `LargestDirectoriesAsync(sourceId, parentId)` aus dem Index. Nie den kompletten Tree in WPF materialisieren, nie das Dateisystem walken.
|
||
- Balken im Tree relativ zu den **Geschwistern des aktuellen Parents**; in Ranking-Views relativ zur Liste.
|
||
- Biggest folders/files zeigen vollständigen oder gekürzten Pfad (`PathRules.ShortenDisplay`), Sortierung `aggregate_size` / `size_bytes` DESC.
|
||
- Actions: Open (aktuelles Pane / anderes Split-Pane / neuer Tab), Search within, Rescan (Indexer-Queue), Copy path.
|
||
- Virtualisierte `ListView`, Children-Cap `AnalysisTreeChildTake`.
|
||
|
||
V1 kein Treemap (Canvas/WPF-Heavy). Datenmodell erlaubt Treemap später (`aggregate_size` pro Kind).
|
||
|
||
WinDirStat-Vorteil: **O(log n) Queries statt Disk-Walk**. Nachteil: Zahl so gut wie der Index — deshalb Status „aktuell / veraltet“ sichtbar machen.
|
||
|
||
---
|
||
|
||
## 13. Duplikaterkennung
|
||
|
||
Nicht jede Datei hashen.
|
||
|
||
1. SQL: `GROUP BY size_bytes HAVING COUNT(*) > 1` (nur Present, nicht excluded).
|
||
2. Gruppen in `hash_queue`, große Dateien niedrige Priorität oder umgekehrt je nach User-Ziel.
|
||
3. Partial Hash: erste 64 KiB SHA-256 (`hash_state=1`).
|
||
4. Nur kollidierende Partial-Gruppen: Full-File SHA-256, `content_hash` persistieren.
|
||
5. Worker: `SetThreadPriority` Background, max. 1 sequentieller Reader pro Volume, pausieren wenn Transfer-Queue aktiv.
|
||
|
||
Filter: Source, Pfad-Prefix, Exclude-Liste. Identisch = gleicher Full Hash, nicht gleicher Name.
|
||
|
||
Hardlinks (gleiche FRN): als „gleiche Datei, mehrere Pfade“ markieren, nicht als Duplikat-Müll.
|
||
|
||
V1 der Detector kann nach dem Explorer-MVP kommen; Schema (`content_hash`, `hash_queue`) trotzdem in V1 anlegen, damit kein zweites Migration-Chaos entsteht.
|
||
|
||
---
|
||
|
||
## 14. Offline-Media-Konzept
|
||
|
||
- Source bleibt im Tree, Badge **Offline**, `last_seen_utc`, Kapazität/Label.
|
||
- Entries: `status=Offline` (Volume weg) vs. `Deleted` (USN/Scan hat Löschung gesehen).
|
||
- Navigation: Index-Listing, Suche über Offline-Medien **an**.
|
||
- Dateioperationen: Copy/Move/Delete disabled; Öffnen disabled; Pfad kopieren erlaubt.
|
||
- Wieder da: Fingerprint-Match, `last_root_path` updaten, USN oder Folder-Diff, Status Online.
|
||
|
||
USB mitten im Scan: Job abbrechen/fehlschlagen, committed Batches behalten, Source Offline, Banner „Scan unvollständig“.
|
||
|
||
---
|
||
|
||
## 15. Historienkonzept (speichereffizient)
|
||
|
||
Nicht jede Datei versionieren.
|
||
|
||
**Drei Stufen:**
|
||
|
||
1. **Tombstones** auf `entries` (`status=Deleted`, `deleted_utc`) — Datei existierte. Retention-Policy (sofort / N Tage / dauerhaft).
|
||
2. **Source-Rollups** täglich: eine Zeile in `source_stats_history` (Größe, Counts). Das liefert „+384 GB seit letztem Monat“ ohne Dateihistorie.
|
||
3. **Directory-Rollups** nur für Ordner über Schwellwert (z. B. > 1 GB oder Top 200 je Source) in `directory_stats_history`. Retention: 90 Tage täglich, danach wöchentlich verdichten.
|
||
|
||
Kein Copy-on-Write des ganzen Trees. Kein Git fürs Dateisystem.
|
||
|
||
Spätere UI: Delta-Badge am Ordner, wenn zwei Snapshots existieren. V1 nur Schema + nächtlicher Rollup-Job.
|
||
|
||
---
|
||
|
||
## 16. Fehler- und Recovery-Konzept
|
||
|
||
Prinzip: **Fehler sind lokal, Jobs sind unterbrechbar, die DB überlebt Crashes.**
|
||
|
||
| Ereignis | Verhalten |
|
||
|---|---|
|
||
| Access Denied | `scan_errors`, weiter |
|
||
| Invalid/corrupt dirent | loggen, weiter |
|
||
| Netzwerk weg | Source Offline, Job Failed/Cancelled, Index behalten |
|
||
| USB gezogen | wie Netzwerk; offene Transfers Failed |
|
||
| Locked file (Hash/Copy) | Eintrag/Job Error, Queue weiter |
|
||
| Sehr lange Pfade | `\\?\`, sonst Error-Log |
|
||
| Unicode | UTF-8 in SQLite, Originalname in `name` |
|
||
| UI-Crash während Scan | WAL; letzte Transaktion committed; Scan-Job beim Start als unterbrochen markieren |
|
||
| SQLite I/O error | Writer stoppt, Banner, keine stillen Writes |
|
||
| Cancel | keine halben Batches; `usn_next` nur nach Commit |
|
||
| Scan während Änderungen | USN nach Scan-Ende nachziehen; oder generation-mark + missing → Deleted |
|
||
| Journal lost | Stale + Full Rescan nötig |
|
||
|
||
DB-Integrität: nach Crash `PRAGMA quick_check` beim Start (asynchron, UI nicht blockieren). Backup optional: periodisches `VACUUM INTO` später, nicht V1.
|
||
|
||
Logging: Serilog rolling file unter `%LocalAppData%\…\logs`. Scan-Fehler zusätzlich in DB für die UI-Liste „übersprungene Ordner“.
|
||
|
||
---
|
||
|
||
## 17. Performance-Risiken
|
||
|
||
| Risiko | Mitigation |
|
||
|---|---|
|
||
| UI-Freeze durch Binding von 100k Items | Virtualizing `ListView`/`DataGrid`, Paging, nie ganze Source materialisieren |
|
||
| SQLite Writer-Stau | Ein Writer, große Batches, keine UI-Reads im Writer |
|
||
| FTS5 bloat | External content table, `optimize` nach Full Scan |
|
||
| Watcher-Flood | Coalesce 300–500 ms, Overflow → Stale |
|
||
| SMB-Latenz | Live-Listing async, Placeholder-Zeilen, Timeout |
|
||
| Recursive LIKE ohne Index | Immer `source_id` zuerst; Prefix-LIKE nur mit Index auf `path_rel` |
|
||
| Aggregate-Update-Ketten bei Massen-Delete | Batch-Delta pro Parent statt N einzelne UPDATEs wo möglich |
|
||
| RAM | Streaming Enum, kein `List<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 (Dropbox) und PluginHost/IPC
|
||
- Explorer.exe ersetzen
|
||
- Volltext in Dateiinhalten
|
||
|
||
### Reparse-Policy (V1 festlegen)
|
||
|
||
| Typ | Default |
|
||
|---|---|
|
||
| Directory Junction / Directory Symlink | **Nicht folgen**; als Link-Eintrag indexieren, Overlay-Icon |
|
||
| Volume Mount Point | Als **eigene Source** behandeln, wenn Volume-GUID bekannt; sonst nicht in den Baum des Parents mergen |
|
||
| File Symlink | Als Datei indexieren, Größe des Links; Hash nicht dem Target folgen |
|
||
| Andere Reparse (OneDrive placeholder, Dedup) | Als Datei/Ordner mit Tag; Enumeration normal, wenn Win32 sie als Datei zeigt |
|
||
|
||
Schleifen: besuchte `(volume_guid, FRN)`-Menge während eines Scans. Zweites Betreten → skip.
|
||
|
||
---
|
||
|
||
## 19. Entwicklungsphasen
|
||
|
||
### Phase 0 — Gerüst (ca. 1 Woche)
|
||
|
||
Solution, DI, Logging, leeres Fenster, Themes, SQLite-Ping, Volume-Enumeration ohne Index.
|
||
|
||
### Phase 1 — Live-Explorer (2–3 Wochen)
|
||
|
||
Pane, Tree, Breadcrumb, History, Details-View, Virtualization, Basis-Ops ohne Queue, Recycle, Long Paths. **Fühlt sich schon wie Explorer an.**
|
||
|
||
### Phase 2 — Tabs, Split, Queue (2 Wochen)
|
||
|
||
Tabs, Dual Pane, DnD, Transfer-Queue, Progress-Panel.
|
||
|
||
### Phase 3 — Index-MVP (3 Wochen)
|
||
|
||
Schema, Source-Identität, Full Scan, Excludes, Aggregates, Offline, Ordnergrößen-Spalte, Suche über Index, Refresh/Rescan.
|
||
|
||
### Phase 4 — Change Tracking (2 Wochen)
|
||
|
||
USN wo möglich, Watcher+Folder-Diff Fallback, Stale-Logik, Resume-Scan.
|
||
|
||
### Phase 5 — Analyse (1–2 Wochen)
|
||
|
||
Analyse-View, Balken, Drilldown, Typ-Aggregation.
|
||
|
||
### Phase 6 — Duplikate + Historie (2 Wochen)
|
||
|
||
Hash-Pipeline, Duplikat-UI, Rollups, Tombstone-Retention-Settings.
|
||
|
||
### Phase 7 — Härten
|
||
|
||
SMB-Stabilität, Elevation-optional, Query-Syntax, Dienst-Schnittstelle (`Explorer.Contracts` IPC), Installer.
|
||
|
||
Phasen 1–3 sind das kleinste lieferbare Produkt, das die Vision trägt. USN ist bewusst Phase 4: ohne soliden Scan und Identity ist das Journal wertlos.
|
||
|
||
---
|
||
|
||
## 20. Entscheidungen vor Implementierungsbeginn
|
||
|
||
Bitte diese Punkte klären. Empfohlene Defaults in Klammern:
|
||
|
||
1. **Produktname und AppId** (Arbeitsname Explorer ist ungeeignet für Store/Mutex/`LocalAppData`).
|
||
2. **Hybrid Live-FS + Index** vs. Index-first Listing (**Hybrid**).
|
||
3. **Elevation:** nie Admin / optional USN-Dienst / App immer elevatet (**USN opportunistisch, kein Pflicht-Admin**).
|
||
4. **Split-Default:** neues Tab single oder immer dual (**single, Split per Tab**).
|
||
5. **Tombstones default:** sofort löschen / 30 Tage / dauerhaft (**30 Tage**).
|
||
6. **Erstes Laufwerk:** Banner „Index aufbauen“ vs. stiller Scan (**Banner, User startet Scan**).
|
||
7. **Sprache UI:** de / en / beide (**de + en, System-UI**).
|
||
8. **Distribution:** portable ZIP / MSIX / Setup (**MSIX oder Setup, DB unter LocalAppData**).
|
||
9. **Mindest-OS:** Windows 10 22H2 vs. 11 only (**Windows 10 1809+ / 11**).
|
||
10. **WPF-Fluent-Kit:** WPF-UI (lepoco) vs. eigene ResourceDictionaries (**WPF-UI für Tempo, austauschbar hinter Themes**).
|
||
11. **Copy-Engine:** nur IFileOperation vs. CopyFileEx-Queue (**Mischung, siehe §11**).
|
||
12. **Netzwerk-Shares im Tree:** nur manuell vs. zuletzt verwendete vs. Windows-Netzwerkumgebung (**manuell + Recents**).
|
||
|
||
---
|
||
|
||
## Hintergrundprozess: Explorer.Host.exe
|
||
|
||
Indexing, USN/watchers, transfer queue, hash worker, and history rollup run in **`Explorer.Host.exe`**, not in the WPF window. Idle-aware background maintenance is coordinated there as well (`GetLastInputInfo` / power status — no WPF dependency).
|
||
|
||
| | Explorer.Host.exe (current) | Windows Service |
|
||
|---|---|---|
|
||
| Complexity | per-user process, named pipe | Session 0, ACL, service updates |
|
||
| Index when the window is closed | continues | continues |
|
||
| Rights | same user as the window | typically SYSTEM |
|
||
| Crash isolation | window crash does not stop the index | isolated |
|
||
| Autostart | optional HKCU Run (current user, no elevation) | service start |
|
||
| Used | **yes** | **not used** |
|
||
|
||
The window (`Explorer.App.exe`) opens the SQLite index **read-only** (WAL). Only the host opens it for write. `Explorer.Contracts` (`IWorkbenchHost`, `ICloudOverlay`, `IHostConnection`) is the IPC surface over a current-user named pipe. Plugin implementations load in the host; the window talks overlay through the pipe.
|
||
|
||
If the host is not running, the window starts `Explorer.Host.exe` beside itself. Closing the window does not stop the host. Quit it from the host tray (**Quit background host**) or **File → Stop background host…**. Settings can add the host to the current user’s Windows sign-in programs (`HKCU\...\Run`) so it starts at logon without administrator rights.
|
||
|
||
The original V1 sketch was in-process `IHostedService` inside the UI. That path is gone. A Windows Service is still out of scope.
|
||
|
||
---
|
||
|
||
## 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** — indexer, transfer queue, hash worker, and watchers run as `IHostedService` instances inside **`Explorer.Host.exe`**. `BackgroundMaintenanceCoordinator` is the one extra 1s timer: it uses `IUserIdleMonitor` / `BackgroundWorkPolicy` to pause or resume duplicate hashing, enqueue at most one idle local full scan (`IndexWorkOrigin.Idle`, distinct from user/watcher/USN work), and call `HistoryRollupService` (no longer its own hosted loop). Copy/move/delete and explicit scans are never idle work. The WPF window is a named-pipe client (`Explorer.Hosting.Client`) with a read-only SQLite store. No Windows Service; optional current-user sign-in (`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`).
|
||
7. **Search syntax** — structured `SearchQuery` exists; Everything-like lexer is not shipped (Phase 7).
|
||
8. **UNIQUE identity** — `UNIQUE (source_id, ifnull(parent_id,-1), name_norm)` because SQLite UNIQUE treats NULLs as distinct.
|
||
9. **INSERT ids** — `Microsoft.Data.Sqlite` + Dapper `ExecuteScalarAsync` on `INSERT … RETURNING` leaves the write connection busy and hangs the next command. Writer-connection SQL uses `SqliteCommand` (`SqliteExec`) and `last_insert_rowid()`.
|
||
10. **SQLite cache** — `SqliteCacheMode.Shared` deadlocked reader+writer connections in-process. Connections use the default private cache.
|
||
11. **Schema apply** — Microsoft.Data.Sqlite/`Execute` splitting on `;` broke `CREATE TRIGGER` bodies. Schema is applied as an explicit statement array (`SchemaScript.Statements`).
|
||
12. **Search CurrentFolder** — `SearchRequest.DirectChildrenOnly` restricts to the folder’s children in SQL (not a client-side filter after paging).
|
||
13. **Duplicate hashing** — full-file hashes run only when another same-size file already shares the partial hash. Unique partial hashes skip the full read.
|
||
14. **Index freshness** — folder sizes in the listing come from `aggregate_size`. Watcher reconcile stays one directory deep. Opening a folder enqueues a 1-level reconcile of that path, then `FolderDisplayRefresh` probes up to 8 largest/visible child dirs (child count). Mismatches get `EnqueueVerify` for that child only and are cancelled when the browse generation changes. Idle maintenance still verifies each local indexed source (deep walk, cap 48). USN directory deletes also tombstone the path prefix.
|
||
15. **Browse names first** — live folder listing starts without waiting for source lookup, archive index, `MarkReachable`, or child-index overlay. The first name is published immediately (`BrowseHydration.FirstPublish`); folder sizes, cloud state, and Git badges arrive as later `BrowseDelta` updates. Archive paths still resolve through the index before live enumeration.
|
||
16. **Shell context verbs** — the compact item menu only reads static registry verbs (no `IContextMenu` on right-click; that AV’d via `IShellFolder.GetUIObjectOf` / `MENUITEMINFO` string marshaling). **Open in Notepad++** is a first-class Workbench command (same pattern as **Open in Cursor**), not a shell-extension row. `IContextMenu` / `TrackPopupMenu` is not used on the compact menu.
|
||
|