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>
898 lines
40 KiB
Markdown
898 lines
40 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, 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 **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 (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:
|
||
|
||
1. **Produktname und AppId** (Arbeitsname Explorer ist ungeeignet für Store/Mutex/`LocalAppData`).
|
||
2. **Hybrid Live-FS + Index** vs. Index-first Listing (**Hybrid**).
|
||
3. **Elevation:** nie Admin / optional USN-Dienst / App immer elevatet (**USN opportunistisch, kein Pflicht-Admin**).
|
||
4. **Split-Default:** neues Tab single oder immer dual (**single, Split per Tab**).
|
||
5. **Tombstones default:** sofort löschen / 30 Tage / dauerhaft (**30 Tage**).
|
||
6. **Erstes Laufwerk:** Banner „Index aufbauen“ vs. stiller Scan (**Banner, User startet Scan**).
|
||
7. **Sprache UI:** de / en / beide (**de + en, System-UI**).
|
||
8. **Distribution:** portable ZIP / MSIX / Setup (**MSIX oder Setup, DB unter LocalAppData**).
|
||
9. **Mindest-OS:** Windows 10 22H2 vs. 11 only (**Windows 10 1809+ / 11**).
|
||
10. **WPF-Fluent-Kit:** WPF-UI (lepoco) vs. eigene ResourceDictionaries (**WPF-UI für Tempo, austauschbar hinter Themes**).
|
||
11. **Copy-Engine:** nur IFileOperation vs. CopyFileEx-Queue (**Mischung, siehe §11**).
|
||
12. **Netzwerk-Shares im Tree:** nur manuell vs. zuletzt verwendete vs. Windows-Netzwerkumgebung (**manuell + Recents**).
|
||
|
||
---
|
||
|
||
## Hintergrunddienst: A vs. B
|
||
|
||
| | A In-Process (V1) | B Windows Service (später) |
|
||
|---|---|---|
|
||
| Komplexität | niedrig | Session 0, ACL, IPC, Updates |
|
||
| Index wenn UI zu | stoppt | läuft weiter |
|
||
| USN-Rechte | oft unzureichend | SYSTEM kann Journal lesen |
|
||
| Crash-Isolation | UI-Crash stoppt Index | getrennt |
|
||
| Empfohlen | **V1 = A** | **V2 = B**, wenn Identity+Schema stabil sind |
|
||
|
||
V1 so schneiden, dass der Indexer ein `IHostedService` ist. Im Dienst-Host später derselbe Service, UI spricht über Named Pipe / gRPC (`Explorer.Contracts`). Nicht in V1 bauen, Interfaces nicht an WPF kleben.
|
||
|
||
---
|
||
|
||
## Sicherheit und Robustheit (kurz)
|
||
|
||
- Keine Indexierung von Inhalten geschützter Ordner ohne User-Exclude-Defaults: `C:\Windows`, `C:\System Volume Information`, `$Recycle.Bin`, `C:\ProgramData\Microsoft`, Standard-Temp.
|
||
- Scans laufen in User-Rechten; nicht heimlich SeBackupPrivilege fordern.
|
||
- DB liegt im User-Profil, nicht world-writable.
|
||
- UNC-Pfade nicht als ausführbare Kommandos interpretieren.
|
||
- Hash-Worker lesen nur; keine Writes ins FS.
|
||
|
||
---
|
||
|
||
## UX-Sprache (verbindlich)
|
||
|
||
| Intern | UI |
|
||
|---|---|
|
||
| Source | Laufwerk, Medium, Speicherort |
|
||
| Entry | Datei, Ordner |
|
||
| Full Scan | Index aufbauen / Vollständig einlesen |
|
||
| Stale | Möglicherweise nicht aktuell |
|
||
| Tombstone | Früher vorhanden / Nicht mehr vorhanden |
|
||
| FTS / SQLite | — (nie zeigen) |
|
||
| USN | — (nie zeigen; höchstens „Änderungsprotokoll des Laufwerks“) |
|
||
|
||
Statuszeile darf „1,2 Mio. Dateien indexiert · aktuell“ sagen. Das ist Explorer-Gedächtnis, kein Datenbankprodukt.
|
||
|
||
---
|
||
|
||
## Implementation notes (this codebase)
|
||
|
||
Deviations from the design above, with reasons:
|
||
|
||
1. **`entries.scan_generation`** — added so a full/folder scan can mark unseen rows as deleted without keeping the whole tree in RAM. UPSERT stamps the generation; missing rows with an older generation become tombstones.
|
||
2. **CsWin32** — not used. Win32 is called via `LibraryImport`/`DllImport` in `Explorer.Windows`. CsWin32 generated code was heavier than needed for the small API surface (volumes, USN, `CopyFileEx`, Recycle Bin).
|
||
3. **WPF-UI (lepoco)** — not used. Light/Dark Fluent-style brushes live in `Themes/Dark.xaml` and `Themes/Light.xaml` so the UI toolkit stays replaceable.
|
||
4. **`PRAGMA mmap_size` / large `cache_size`** — not applied at runtime. They made SQLite native startup unreliable under concurrent test hosts; WAL + `synchronous=NORMAL` remain.
|
||
5. **App data folder** — `%LocalAppData%\ExplorerWorkbench` (not `Explorer`) so the working name does not collide with Windows Explorer.
|
||
6. **Background work** — in-process `IHostedService` instances (indexer, transfer queue, hash worker, history rollup, watchers). No Windows Service in this run.
|
||
7. **Search syntax** — structured `SearchQuery` exists; Everything-like lexer is not shipped (Phase 7).
|
||
8. **UNIQUE identity** — `UNIQUE (source_id, ifnull(parent_id,-1), name_norm)` because SQLite UNIQUE treats NULLs as distinct.
|
||
9. **INSERT ids** — `Microsoft.Data.Sqlite` + Dapper `ExecuteScalarAsync` on `INSERT … RETURNING` leaves the write connection busy and hangs the next command. Writer-connection SQL uses `SqliteCommand` (`SqliteExec`) and `last_insert_rowid()`.
|
||
10. **SQLite cache** — `SqliteCacheMode.Shared` deadlocked reader+writer connections in-process. Connections use the default private cache.
|
||
11. **Schema apply** — Microsoft.Data.Sqlite/`Execute` splitting on `;` broke `CREATE TRIGGER` bodies. Schema is applied as an explicit statement array (`SchemaScript.Statements`).
|
||
12. **Search CurrentFolder** — `SearchRequest.DirectChildrenOnly` restricts to the folder’s children in SQL (not a client-side filter after paging).
|
||
13. **Duplicate hashing** — full-file hashes run only when another same-size file already shares the partial hash. Unique partial hashes skip the full read.
|
||
|