docs: Rechercheergebnisse zum Notiz-Handling uebernehmen
Beide Recherchen zur Map #16 lagen nur auf lokalen Wegwerf-Branches. Die Kurzfassungen stehen an den Tickets #21 und #22, die Langfassungen sind mit Quellen und der Trennung zwischen Belegtem, Abgeleitetem und Unbelegtem aber das, worauf man zurueckkommt. Beide enthalten Messungen, die keine Dokumentation hergibt: dass trash 5.2.6 auf Netzlaufwerken gar nicht funktioniert und Windows dort sonst still und endgueltig loeschen wuerde, und dass ein ungeprunter Append-Log im Yjs-Benchmark das Siebzigfache des Rohtexts misst. Ausserdem /.claude in die .gitignore - dort liegen lokale Einstellungen und die temporaeren Worktrees der Recherche-Agenten.
This commit is contained in:
575
docs/research/papierkorb.md
Normal file
575
docs/research/papierkorb.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# Papierkorb unter Rust: Crate, Netzlaufwerke, Kosten
|
||||
|
||||
**Gitea-Issue:** #21 · **Stand der Recherche:** 2026-08-28
|
||||
**Umfeld:** DioNotes, Rust (edition 2021), Dioxus 0.7.1, Default-Feature `desktop`.
|
||||
Entwicklung und Messung unter Windows 11 Enterprise 10.0.26200, cargo/rustc 1.96.0.
|
||||
|
||||
Alle Aussagen sind als **[belegt]** (Primärquelle mit URL, Quellcode, oder eigene reproduzierbare
|
||||
Messung) oder **[Vermutung]** (Schlussfolgerung ohne deckende Quelle) markiert.
|
||||
|
||||
> **Besonderheit dieser Recherche:** Die Kernfrage — was auf Netzlaufwerken passiert — wurde
|
||||
> **nicht erschlossen, sondern gemessen**. Auf dem Entwicklungsrechner sind SMB-Netzlaufwerke
|
||||
> gemountet (`Z:` → `\\la-nas-01.ad.datawin.de\Scan`). Es wurden drei kleine Wegwerf-Programme
|
||||
> außerhalb des Repos gebaut und ausgeführt. Die Rohausgaben stehen in Abschnitt 4.
|
||||
> Im Repo wurde **keine Dependency hinzugefügt**; `Cargo.toml` und `Cargo.lock` sind unverändert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Kurzantwort
|
||||
|
||||
**Die Crate ist `trash`, aktuell `5.2.6`** (veröffentlicht 2026-05-03). Sie ist der De-facto-Standard
|
||||
und wird gepflegt.
|
||||
|
||||
**Aufruf:**
|
||||
|
||||
```rust
|
||||
trash::delete(&pfad)?; // eine Datei
|
||||
trash::delete_all(&pfade)?; // mehrere
|
||||
```
|
||||
|
||||
**Kosten:** **+6 Pakete** im Windows-Graphen. Das ist **nicht** über Cargo-Features reduzierbar —
|
||||
das im Ticket vermutete Gegenstück zum `rfd`/Wayland-Fall **gibt es hier nicht**. Fünf der sechs
|
||||
Pakete sind eine **zweite, ältere Kopie der `windows`-Crate-Familie** (0.56 neben dem bereits
|
||||
vorhandenen 0.61.3), weil `trash 5.2.6` noch auf `windows 0.56.0` festgenagelt ist.
|
||||
|
||||
**Netzlaufwerke — und hier wird es unangenehm, es sind zwei getrennte Probleme:**
|
||||
|
||||
1. **`trash 5.2.6` funktioniert auf Netzlaufwerken schlicht gar nicht.** Der Aufruf schlägt mit
|
||||
einer irreführenden Meldung fehl (`0x80070002`, „Das System kann die angegebene Datei nicht
|
||||
finden"), **die Datei bleibt unangetastet liegen**. Ursache ist ein Pfad-Bug in der Crate, kein
|
||||
Papierkorb-Thema. Immerhin: **nichts geht verloren.**
|
||||
|
||||
2. **Wäre der Bug behoben, wäre das Verhalten gefährlich.** Ruft man dieselbe Windows-API mit
|
||||
denselben Flags, aber unverbogenem Pfad auf, dann wird die Datei auf dem Netzlaufwerk
|
||||
**endgültig und stillschweigend gelöscht** — `PerformOperations()` liefert `Ok(())`,
|
||||
`GetAnyOperationsAborted()` liefert `false`. Am Rückgabewert ist der Fall **nicht** vom
|
||||
erfolgreichen Papierkorb-Verschieben zu unterscheiden.
|
||||
|
||||
**Die Antwort auf die eigentliche Ticketfrage lautet also: Ja, die App braucht eine Vorab-Prüfung
|
||||
bzw. Rückfrage.** Sich auf `Ok(())` zu verlassen, reicht nicht.
|
||||
|
||||
**Erkennen lässt sich der Fall zuverlässig** — aber nur über `IFileOperationProgressSink`, den
|
||||
`trash` nicht durchreicht (die Crate übergibt hart `None`). Details in Abschnitt 5.
|
||||
|
||||
**Blockierend.** Die Dauer ist unabhängig von der Dateigröße (Verschieben innerhalb des Volumes);
|
||||
gemessen: ~740 ms beim allerersten Aufruf (COM-Initialisierung), danach ~27 ms. Abschnitt 6.
|
||||
|
||||
---
|
||||
|
||||
## 2. Welche Crate, wird sie gepflegt?
|
||||
|
||||
### [belegt]
|
||||
|
||||
- **`trash`, aktuelle Version `5.2.6`**, veröffentlicht **2026-05-03**, nicht zurückgezogen.
|
||||
2.683.466 Downloads gesamt, 724.229 „recent". Beschreibung: „A library for moving files and
|
||||
folders to the Recycle Bin".
|
||||
→ <https://crates.io/api/v1/crates/trash>
|
||||
- Letzte Releases: 5.2.6 (2026-05-03), 5.2.5 (2025-10-25), 5.2.4 (2025-10-24), 5.2.3 (2025-08-15),
|
||||
5.2.2 (2025-02-22). Also **regelmäßige Releases, zuletzt vor knapp vier Monaten**.
|
||||
→ <https://crates.io/api/v1/crates/trash>
|
||||
- Das in `Cargo.toml` eingetragene Repository ist `https://github.com/ArturKovacs/trash`; gepflegt
|
||||
und released wird faktisch aus dem Fork **<https://github.com/Byron/trash-rs>** (251 Sterne), auf
|
||||
den auch die CI-Badges und alle Changelog-Commit-Links zeigen.
|
||||
→ `trash-5.2.6/README.md`, `trash-5.2.6/CHANGELOG.md`
|
||||
- Unterstützte Plattformen laut README: „Windows, macOS, and all FreeDesktop Trash compliant
|
||||
environments (including GNOME, KDE, XFCE, and more)".
|
||||
→ `trash-5.2.6/README.md`
|
||||
- `rust-version = "1.85.0"`, `edition = "2021"`, Lizenz MIT.
|
||||
→ `trash-5.2.6/Cargo.toml`
|
||||
- Das Changelog von 5.2.6 zeigt echte, inhaltliche Wartung (Panic durch sauberen Fehler ersetzt,
|
||||
weil `cosmic-files` abstürzte) — die Crate hat reale, anspruchsvolle Nutzer.
|
||||
→ `trash-5.2.6/CHANGELOG.md`
|
||||
|
||||
### [Vermutung]
|
||||
|
||||
Es gibt keine ernstzunehmende Alternative. `rrecycle` und `recyclebin` tauchen in der Suche auf,
|
||||
sind aber ungleich kleiner und teils reine CLI-Werkzeuge. Für DioNotes ist `trash` gesetzt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was kostet das im Abhängigkeitsgraphen?
|
||||
|
||||
Das Ticket fragt gezielt nach einem Gegenstück zum `rfd`-Fall, wo sich neun Wayland-Pakete durch
|
||||
Angleichen der Features an `dioxus-desktop` vollständig vermeiden ließen.
|
||||
|
||||
### [belegt] Antwort: Ein solches Gegenstück gibt es nicht.
|
||||
|
||||
Die Features von `trash 5.2.6` lauten wörtlich:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
coinit_apartmentthreaded = []
|
||||
coinit_disable_ole1dde = []
|
||||
coinit_multithreaded = []
|
||||
coinit_speed_over_memory = []
|
||||
default = ["coinit_apartmentthreaded", "chrono"]
|
||||
```
|
||||
→ `trash-5.2.6/Cargo.toml`
|
||||
|
||||
**Alle vier `coinit_*`-Features haben leere Dependency-Listen** — es sind reine `cfg`-Schalter für
|
||||
den COM-Initialisierungsmodus (`COINIT_APARTMENTTHREADED` vs. `COINIT_MULTITHREADED`), ohne
|
||||
jede Wirkung auf den Graphen.
|
||||
→ `trash-5.2.6/src/windows.rs`, `struct CoInitializer`
|
||||
|
||||
Das einzige dependency-wirksame Feature ist `chrono`, und das ist **ausschließlich unter
|
||||
`cfg(all(unix, not(macos), not(ios), not(android)))`** deklariert. **Auf Windows ist es
|
||||
wirkungslos.**
|
||||
→ `trash-5.2.6/Cargo.toml`, Abschnitt `[target.'cfg(all(unix, …))'.dependencies.chrono]`
|
||||
|
||||
### [belegt] Gemessene Zahlen
|
||||
|
||||
Basis: `Cargo.lock` im Repo mit 745 Paketen; `cargo tree --edges normal` ergibt 419 eindeutige
|
||||
Pakete für `x86_64-pc-windows-msvc` und 470 für `x86_64-unknown-linux-gnu`.
|
||||
|
||||
| Variante | Windows-Target | Linux-Target |
|
||||
|---|---|---|
|
||||
| `cargo add trash` (default) | **+6** (419 → 425) | +4 (470 → 474) |
|
||||
| `cargo add trash --no-default-features` | **+6** (keine Ersparnis) | +2 (470 → 472) |
|
||||
|
||||
Die sechs neuen Pakete auf Windows:
|
||||
|
||||
```
|
||||
trash v5.2.6
|
||||
windows v0.56.0
|
||||
windows-core v0.56.0
|
||||
windows-implement v0.56.0 (proc-macro)
|
||||
windows-interface v0.56.0 (proc-macro)
|
||||
windows-result v0.1.2
|
||||
```
|
||||
|
||||
### [belegt] Warum die `windows`-Pakete nicht „gratis" sind
|
||||
|
||||
Sie stehen zwar bereits namentlich im `Cargo.lock`, aber in **semver-inkompatiblen** Versionen,
|
||||
werden also **nicht** dedupliziert:
|
||||
|
||||
| Crate | schon im Lock (via dioxus/wry) | von `trash` gefordert | dedupliziert? |
|
||||
|---|---|---|---|
|
||||
| `windows` | 0.61.3 | **0.56.0** | nein |
|
||||
| `windows-core` | 0.61.2 | **0.56.0** | nein |
|
||||
| `windows-implement` | 0.60.2 | **0.56.0** | nein |
|
||||
| `windows-interface` | 0.59.3 | **0.56.0** | nein |
|
||||
| `windows-result` | 0.3.4 | **0.1.2** | nein |
|
||||
|
||||
`cargo tree -i windows@0.56.0` bestätigt: `trash` ist der einzige Konsument dieser zweiten Kopie.
|
||||
|
||||
Wirklich gratis sind nur `log` (0.4.33) und `scopeguard` (1.2.0), die bereits passend im Graphen
|
||||
stehen.
|
||||
|
||||
### [belegt] Auf Linux kostet es fast nichts
|
||||
|
||||
Die FreeDesktop-Implementierung ist reines Rust (`libc`, `once_cell`, `scopeguard`, `urlencoding`,
|
||||
optional `chrono`). **Kein GTK, kein dbus, kein zbus** — der `rfd`-artige Fallstrick existiert hier
|
||||
nicht.
|
||||
→ `trash-5.2.6/Cargo.toml`
|
||||
|
||||
Mit `--no-default-features` fielen auf Linux `chrono` + `iana-time-zone` weg (+2 statt +4). Preis:
|
||||
`trash` kann dann keinen `DeletionDate`-Zeitstempel in die `.trashinfo`-Dateien schreiben. Das ist
|
||||
funktional relevant, nicht bloß kosmetisch. **[Vermutung]** Für DioNotes wäre der Verzicht auf zwei
|
||||
Pakete den Verlust der Zeitstempel nicht wert.
|
||||
|
||||
### [Vermutung] Der einzige echte Hebel
|
||||
|
||||
Ein künftiges `trash`-Release, das auf `windows` ≥ 0.61 aktualisiert, würde die gesamte
|
||||
`windows`-Familie mit dem bestehenden dioxus/wry-Graphen deduplizieren. Der Windows-Delta fiele
|
||||
dann von 6 auf **1** (nur `trash` selbst). Das ist nichts, worauf man warten sollte, aber es ist der
|
||||
Grund, die Version im Blick zu behalten.
|
||||
|
||||
---
|
||||
|
||||
## 4. Netzlaufwerke und USB-Sticks — die Kernfrage
|
||||
|
||||
Das Ticket fragt: Fällt der Aufruf still auf endgültiges Löschen zurück, oder schlägt er fehl?
|
||||
|
||||
**Die Antwort hat zwei Schichten, und sie sind unabhängig voneinander.**
|
||||
|
||||
### 4.1 [belegt] Schicht 1: `trash 5.2.6` schlägt auf Netzlaufwerken fehl — wegen eines Pfad-Bugs
|
||||
|
||||
Gemessen, Wegwerf-Programm gegen `trash 5.2.6`, Datei auf `Z:\` (SMB-Share):
|
||||
|
||||
```
|
||||
=== C: Netzlaufwerk SMB, klein | dir=Z:\ | 1024 bytes ===
|
||||
angelegt: Z:\dionotes-trashprobe-net-small.md
|
||||
trash::delete -> Err(Os { code: -2147024894,
|
||||
description: "windows error: Das System kann die angegebene Datei nicht finden. (0x80070002)" })
|
||||
Dauer: 31.0407ms
|
||||
Datei existiert noch? true
|
||||
Papierkorb-Treffer: vorher=0 nachher=0 => IM PAPIERKORB: false
|
||||
```
|
||||
|
||||
Zum Vergleich, dieselbe Operation lokal auf NTFS:
|
||||
|
||||
```
|
||||
=== A: lokal NTFS, klein | dir=C:\…\Temp | 1024 bytes ===
|
||||
trash::delete -> Ok(())
|
||||
Dauer: 740.7595ms
|
||||
Datei existiert noch? false
|
||||
Papierkorb-Treffer: vorher=0 nachher=1 => IM PAPIERKORB: true
|
||||
```
|
||||
|
||||
**Die Datei auf dem Netzlaufwerk bleibt liegen. Es geht nichts verloren, aber es passiert auch
|
||||
nichts.** Das reproduziert exakt den seit 2022-08-07 offenen Bug
|
||||
[trash-rs#55 „Unable to delete file on Windows shared folder"](https://github.com/Byron/trash-rs/issues/55)
|
||||
(Label `help wanted`, bis heute ungelöst).
|
||||
|
||||
**Die Ursache, aus dem Quellcode und nachgemessen:**
|
||||
|
||||
`trash` kanonisiert jeden Pfad vor dem Löschen mit `std::fs::canonicalize`:
|
||||
|
||||
```rust
|
||||
let canonical_parent = parent.canonicalize()
|
||||
.map_err(|_| Error::CanonicalizePath { original: parent.to_owned() })?;
|
||||
```
|
||||
→ `trash-5.2.6/src/lib.rs`, `fn canonicalize_paths`
|
||||
|
||||
`windows.rs` schneidet danach stumpf die ersten vier UTF-16-Zeichen `\\?\` ab:
|
||||
|
||||
```rust
|
||||
let path_prefix = ['\\' as u16, '\\' as u16, '?' as u16, '\\' as u16];
|
||||
let wide_path_container = to_wide_path(full_path);
|
||||
let wide_path_slice = if wide_path_container.starts_with(&path_prefix) {
|
||||
&wide_path_container[path_prefix.len()..]
|
||||
} else {
|
||||
&wide_path_container[0..]
|
||||
};
|
||||
```
|
||||
→ `trash-5.2.6/src/windows.rs`
|
||||
|
||||
Das geht für lokale Pfade gut und für UNC-Pfade schief. Gemessen:
|
||||
|
||||
```
|
||||
### canonicalize-Verhalten fuer lokal C:
|
||||
canonicalize() = "\\?\C:\Users\marvin.dietermann\AppData\Local\Temp"
|
||||
nach dem Prefix-Stripping = "C:\Users\marvin.dietermann\AppData\Local\Temp" <- gültig
|
||||
|
||||
### canonicalize-Verhalten fuer Netzlaufwerk Z:
|
||||
canonicalize() = "\\?\UNC\la-nas-01.ad.datawin.de\Scan"
|
||||
nach dem Prefix-Stripping = "UNC\la-nas-01.ad.datawin.de\Scan" <- Unsinn
|
||||
|
||||
### canonicalize-Verhalten fuer UNC-Pfad \\la-nas-01.ad.datawin.de\Scan
|
||||
canonicalize() = "\\?\UNC\la-nas-01.ad.datawin.de\Scan"
|
||||
nach dem Prefix-Stripping = "UNC\la-nas-01.ad.datawin.de\Scan" <- Unsinn
|
||||
```
|
||||
|
||||
Aus `\\?\UNC\server\share` wird `UNC\server\share` — ein Pfad, den es nicht gibt.
|
||||
`SHCreateItemFromParsingName` scheitert daran mit `0x80070002`. Korrekt wäre die Rückübersetzung
|
||||
nach `\\server\share`.
|
||||
|
||||
**Betroffen sind beide Schreibweisen** — der gemappte Laufwerksbuchstabe `Z:\` genauso wie der
|
||||
direkte UNC-Pfad, weil `canonicalize` beide auf dieselbe `\\?\UNC\…`-Form abbildet.
|
||||
|
||||
### 4.2 [belegt] Schicht 2: Windows selbst löscht auf Netzlaufwerken still und endgültig
|
||||
|
||||
Um Crate-Bug von Betriebssystemverhalten zu trennen, wurde `IFileOperation` **direkt** aufgerufen —
|
||||
mit exakt denselben Flags wie `trash` (`FOF_NO_UI | FOF_ALLOWUNDO | FOF_WANTNUKEWARNING`,
|
||||
zusammen `0x00004654`), aber mit unverbogenem Pfad:
|
||||
|
||||
```
|
||||
### IFileOperation direkt auf lokal C:
|
||||
PerformOperations -> Ok(()) (Dauer 102.5266ms)
|
||||
GetAnyOperationsAborted -> false
|
||||
Datei existiert noch? false
|
||||
Papierkorb-Treffer: 1 <- im Papierkorb
|
||||
|
||||
### IFileOperation direkt auf Netzlaufwerk Z:
|
||||
PerformOperations -> Ok(()) (Dauer 405.4515ms)
|
||||
GetAnyOperationsAborted -> false
|
||||
Datei existiert noch? false
|
||||
Papierkorb-Treffer: 0 <- WEG. endgueltig.
|
||||
|
||||
### IFileOperation direkt auf UNC-Pfad \\la-nas-01.ad.datawin.de\Scan
|
||||
PerformOperations -> Ok(()) (Dauer 299.5401ms)
|
||||
GetAnyOperationsAborted -> false
|
||||
Datei existiert noch? false
|
||||
Papierkorb-Treffer: 0 <- WEG. endgueltig.
|
||||
```
|
||||
|
||||
**Das ist die eigentliche Warnung des Tickets.** Auf dem Netzlaufwerk wird die Datei endgültig
|
||||
gelöscht, und die API meldet **denselben Erfolg** wie beim Verschieben in den Papierkorb:
|
||||
`Ok(())` und `GetAnyOperationsAborted() == false`. **Am Rückgabewert ist der Unterschied nicht
|
||||
erkennbar.**
|
||||
|
||||
Bemerkenswert: **`FOF_WANTNUKEWARNING` hat keinen Dialog ausgelöst.** Microsoft dokumentiert das
|
||||
Flag als „Send a warning if a file or folder is being destroyed during a delete operation rather
|
||||
than recycled. This flag partially overrides **FOF_NOCONFIRMATION**."
|
||||
→ <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperation-setoperationflags>
|
||||
|
||||
**[Vermutung]** Die Warnung unterbleibt, weil `trash` zusätzlich `FOF_SILENT` und `FOF_NOERRORUI`
|
||||
setzt: `FOF_NO_UI` ist in der `windows`-Crate als `1556u32` = `0x614` definiert, also
|
||||
`FOF_SILENT (0x4) | FOF_NOCONFIRMATION (0x10) | FOF_NOCONFIRMMKDIR (0x200) | FOF_NOERRORUI (0x400)`.
|
||||
→ `windows-0.56.0/src/Windows/Win32/UI/Shell/mod.rs`, `pub const FOF_NO_UI`
|
||||
Das „partially overrides" bezieht sich laut Doku nur auf `FOF_NOCONFIRMATION`, nicht auf
|
||||
`FOF_SILENT`/`FOF_NOERRORUI`. Belegt ist jedenfalls das Ergebnis: **kein Dialog, keine Rückfrage,
|
||||
Datei weg.**
|
||||
|
||||
### 4.3 USB-Sticks und andere Dateisysteme — [nicht belastbar beantwortet]
|
||||
|
||||
**Das konnte nicht gemessen werden.** Auf dem Testrechner sind zwar zwei Wechseldatenträger
|
||||
(`D:`, `E:`) angemeldet, beide sind aber leer (`FileSystemType = Unknown`, `SizeRemaining = 0`) —
|
||||
offensichtlich Kartenleser ohne Medium. Ohne echten FAT32-/exFAT-Stick lässt sich der Fall nicht
|
||||
verifizieren.
|
||||
|
||||
**[Vermutung]**, ausdrücklich als solche gekennzeichnet: Lokale Wechseldatenträger verhalten sich
|
||||
anders als Netzlaufwerke; Windows legt auf ihnen üblicherweise sehr wohl einen `$RECYCLE.BIN`-
|
||||
bzw. `RECYCLER`-Ordner an. Der kritische Fall ist damit **[Vermutung]** primär das Netzlaufwerk.
|
||||
Verlassen sollte man sich darauf nicht — die in Abschnitt 5 beschriebene Erkennung deckt beide
|
||||
Fälle ab, ohne dass man vorher wissen muss, welcher vorliegt. **Vor der Implementierung sollte
|
||||
dieser Fall mit einem echten USB-Stick nachgemessen werden.**
|
||||
|
||||
Ebenfalls **[Vermutung]**: Auch auf lokalem NTFS kann der Papierkorb fehlen — er lässt sich pro
|
||||
Volume in den Papierkorb-Eigenschaften deaktivieren („Dateien sofort löschen"), und Dateien
|
||||
oberhalb der konfigurierten Maximalgröße gehen ebenfalls direkt verloren. Eine Prüfung, die nur
|
||||
auf den Laufwerkstyp schaut, greift deshalb zu kurz.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wie erfährt das Programm, welcher Fall eingetreten ist?
|
||||
|
||||
### [belegt] Über den Rückgabewert von `trash`: gar nicht
|
||||
|
||||
`trash::delete` liefert `Result<(), Error>`. Die `Error`-Varianten sind `Unknown`, `Os`,
|
||||
`FileSystem` (nur FreeDesktop), `TargetedRoot`, `CouldNotAccess`, `CanonicalizePath`,
|
||||
`ConvertOsString`, `RestoreCollision`, `RestoreTwins`.
|
||||
→ <https://docs.rs/trash/5.2.6/src/trash/lib.rs.html>
|
||||
|
||||
**Es gibt keine Variante und kein Erfolgs-Detail, das „endgültig gelöscht statt Papierkorb"
|
||||
ausdrückt.** Wie Abschnitt 4.2 zeigt, kommt in diesem Fall schlicht `Ok(())` zurück.
|
||||
|
||||
Die Crate wertet lediglich `GetAnyOperationsAborted()` aus, und das war im Netzlaufwerk-Fall
|
||||
`false`:
|
||||
|
||||
```rust
|
||||
pfo.PerformOperations()?;
|
||||
if pfo.GetAnyOperationsAborted()?.as_bool() {
|
||||
// TODO: return the reason why the operation was aborted.
|
||||
return Err(Error::Unknown { description: "Some operations were aborted".into() });
|
||||
}
|
||||
```
|
||||
→ `trash-5.2.6/src/windows.rs`
|
||||
|
||||
### [belegt] Über `IFileOperationProgressSink`: zuverlässig — aber `trash` reicht ihn nicht durch
|
||||
|
||||
Microsoft dokumentiert für `IFileOperationProgressSink::PostDeleteItem` den Parameter
|
||||
`psiNewlyCreated` wörtlich so:
|
||||
|
||||
> „A pointer to an **IShellItem** that specifies the deleted item, now in the Recycle Bin.
|
||||
> **If the item was fully deleted, this value is NULL.**"
|
||||
|
||||
→ <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperationprogresssink-postdeleteitem>
|
||||
|
||||
Genau das wurde nachgemessen — ein eigener Sink, angehängt an `DeleteItem`:
|
||||
|
||||
```
|
||||
### lokal C:
|
||||
[sink] PreDeleteItem dwFlags=0x00000282
|
||||
[sink] PostDeleteItem dwFlags=0x00000282 hrDelete=0x00270008 psiNewlyCreated=Some
|
||||
==> IM PAPIERKORB: true
|
||||
neuerPfad=Some("C:\$Recycle.Bin\S-1-5-21-…-1668\$R1V9IFD.md")
|
||||
|
||||
### Netzlaufwerk Z:
|
||||
[sink] PreDeleteItem dwFlags=0x00000202
|
||||
[sink] PostDeleteItem dwFlags=0x00000202 hrDelete=0x00270008 psiNewlyCreated=NULL
|
||||
==> IM PAPIERKORB: false
|
||||
neuerPfad=None
|
||||
```
|
||||
|
||||
**Zwei verwertbare Signale, beide belegt:**
|
||||
|
||||
1. **`psiNewlyCreated` in `PostDeleteItem`** — `Some` ⇒ im Papierkorb (inklusive konkretem Pfad im
|
||||
`$Recycle.Bin`), `NULL` ⇒ endgültig gelöscht. Das ist die verlässliche Nachher-Auskunft.
|
||||
`hrDelete` taugt **nicht** dafür: es war in beiden Fällen identisch `0x00270008`.
|
||||
|
||||
2. **`dwFlags` schon in `PreDeleteItem`, also *vor* dem Löschen.** Die Differenz zwischen `0x282`
|
||||
und `0x202` ist genau `0x80` — und das ist `TSF_DELETE_RECYCLE_IF_POSSIBLE`:
|
||||
```
|
||||
pub const TSF_DELETE_RECYCLE_IF_POSSIBLE: _TRANSFER_SOURCE_FLAGS = _TRANSFER_SOURCE_FLAGS(128i32);
|
||||
pub const TSF_COPY_LOCALIZED_NAME: _TRANSFER_SOURCE_FLAGS = _TRANSFER_SOURCE_FLAGS(512i32);
|
||||
pub const TSF_OVERWRITE_EXIST: _TRANSFER_SOURCE_FLAGS = _TRANSFER_SOURCE_FLAGS(2i32);
|
||||
```
|
||||
→ `windows-0.56.0/src/Windows/Win32/UI/Shell/mod.rs`
|
||||
|
||||
Auf `C:` ist das Bit gesetzt, auf `Z:` fehlt es. **Windows sagt also im `PreDeleteItem`-Callback
|
||||
von sich aus, ob es recyceln wird** — bevor irgendetwas passiert ist.
|
||||
|
||||
Und man kann an dieser Stelle abbrechen: Microsoft dokumentiert für die Sink-Rückgabewerte
|
||||
„Returns S_OK if successful, or an error value otherwise. In the case of an error value, all
|
||||
subsequent operations pending from the call to IFileOperation are canceled."
|
||||
→ <https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperationprogresssink-postdeleteitem>
|
||||
**[Vermutung]** Damit ließe sich aus `PreDeleteItem` heraus vetoen und stattdessen erst der
|
||||
Nutzer fragen. Der Abbruch aus `PreDeleteItem` wurde **nicht** gemessen.
|
||||
|
||||
**Der Haken:** `trash` übergibt den Sink-Parameter hart als `None`:
|
||||
|
||||
```rust
|
||||
pfo.DeleteItem(&shi, None)?;
|
||||
```
|
||||
→ `trash-5.2.6/src/windows.rs`
|
||||
|
||||
Über die Crate-API ist an diese Information **nicht** heranzukommen.
|
||||
|
||||
### [Vermutung] Konsequenz für DioNotes
|
||||
|
||||
Drei gangbare Wege, in aufsteigender Sauberkeit:
|
||||
|
||||
1. **Billig und grob:** vor dem Löschen prüfen, ob der Workspace-Pfad auf einem Netzlaufwerk liegt
|
||||
(Laufwerkstyp `DRIVE_REMOTE` bzw. UNC-Pfad), und dann eine Rückfrage stellen. Erkennt den
|
||||
Hauptfall, aber nicht den deaktivierten Papierkorb auf lokalem NTFS.
|
||||
2. **Nachher-Prüfung:** nach `trash::delete` per `trash::os_limited::list()` nachsehen, ob der
|
||||
Eintrag im Papierkorb aufgetaucht ist. Funktioniert (so wurden die Messungen oben verifiziert),
|
||||
ist aber langsam und rennt gegen Race Conditions.
|
||||
3. **Sauber:** `IFileOperation` mit eigenem Sink selbst aufrufen, statt `trash` zu benutzen — dann
|
||||
bekommt man `TSF_DELETE_RECYCLE_IF_POSSIBLE` vorab und `psiNewlyCreated` hinterher, und der
|
||||
UNC-Bug aus 4.1 entfällt ebenfalls, weil man `canonicalize` nicht braucht. Kostet dafür eine
|
||||
eigene Windows-Implementierung und hilft auf Linux/macOS nicht.
|
||||
|
||||
Da `trash 5.2.6` auf Netzlaufwerken ohnehin nicht funktioniert, ist die Entscheidung ohne eine
|
||||
bewusste Behandlung dieses Falls **nicht** vollständig — deshalb ist dieses Ticket die
|
||||
Voraussetzung für die Lösch-Implementierung, nicht bloß eine Fußnote.
|
||||
|
||||
---
|
||||
|
||||
## 6. Blockierend oder asynchron? Wie lange dauert es?
|
||||
|
||||
### [belegt] Blockierend
|
||||
|
||||
`trash::delete` ist eine gewöhnliche synchrone Funktion; es gibt keine `async`-Variante in der
|
||||
öffentlichen API. Der Windows-Pfad ruft `pfo.PerformOperations()` direkt im aufrufenden Thread auf,
|
||||
ohne Thread zu spawnen.
|
||||
→ `trash-5.2.6/src/windows.rs`, <https://docs.rs/trash/5.2.6/src/trash/lib.rs.html>
|
||||
|
||||
### [belegt] Gemessene Dauer
|
||||
|
||||
| Fall | Größe | Dauer |
|
||||
|---|---|---|
|
||||
| lokal NTFS, erster Aufruf im Prozess | 1 KiB | **740,8 ms** |
|
||||
| lokal NTFS, zweiter Aufruf | **512 MiB** | **26,7 ms** |
|
||||
| `IFileOperation` direkt, lokal | 5 B | 102,5 ms |
|
||||
| `IFileOperation` direkt, Netzlaufwerk `Z:` | 5 B | 405,5 ms |
|
||||
| `IFileOperation` direkt, UNC-Pfad | 5 B | 299,5 ms |
|
||||
|
||||
**Die Dateigröße spielt praktisch keine Rolle:** 512 MiB wurden in 26,7 ms erledigt — schneller
|
||||
als die 1-KiB-Datei davor. Das Verschieben in den Papierkorb ist ein Rename **innerhalb desselben
|
||||
Volumes**, es werden keine Daten kopiert.
|
||||
|
||||
Was kostet, ist der **erste** Aufruf im Prozess: die COM-Initialisierung
|
||||
(`CoInitializeEx` + `CoCreateInstance` der `FileOperation`-Klasse) schlägt mit gut 0,7 s zu Buche.
|
||||
`trash` initialisiert COM `thread_local` und einmalig.
|
||||
→ `trash-5.2.6/src/windows.rs`, `thread_local! { static CO_INITIALIZER … }`
|
||||
|
||||
**[Vermutung]** Für DioNotes heißt das: der Aufruf gehört nicht in den UI-Thread. Ein
|
||||
`spawn_blocking` bzw. der `tokio`-Threadpool genügt völlig; eine Fortschrittsanzeige braucht es
|
||||
für einzelne `.md`-Dateien nicht. Netzlaufwerke sind mit ~0,3–0,4 s deutlich träger als lokale
|
||||
Platten, aber immer noch weit unter jeder Schmerzgrenze.
|
||||
|
||||
**Achtung [belegt]:** `trash` initialisiert COM standardmäßig mit `COINIT_APARTMENTTHREADED`
|
||||
(Feature `coinit_apartmentthreaded`, Teil der Defaults) und **panickt**, wenn `CoInitializeEx`
|
||||
fehlschlägt:
|
||||
|
||||
```rust
|
||||
panic!("Call to CoInitializeEx failed. HRESULT: {:?}. Consider using `trash` with the feature `coinit_multithreaded`", hr);
|
||||
```
|
||||
→ `trash-5.2.6/src/windows.rs`
|
||||
|
||||
**[Vermutung]** Ruft man `trash` aus einem Tokio-Worker-Thread auf, der bereits als MTA
|
||||
initialisiert wurde, könnte das zuschlagen; dann ist `default-features = false` plus
|
||||
`features = ["coinit_multithreaded"]` der Ausweg. Das wurde **nicht** gemessen.
|
||||
|
||||
---
|
||||
|
||||
## 7. macOS und Linux
|
||||
|
||||
### [belegt] macOS
|
||||
|
||||
`trash` bietet zwei Verfahren, wählbar über `TrashContext`:
|
||||
|
||||
- **`DeleteMethod::Finder` (Default)** — steuert den Finder per AppleScript.
|
||||
- **`DeleteMethod::NsFileManager`** — ruft `trashItemAtURL` auf `NSFileManager` auf. Laut den
|
||||
Doc-Kommentaren: „Somewhat faster than the `Finder` method", „Does *not* require additional
|
||||
permissions", „Does *not* produce the sound that Finder usually makes", aber „Does *not* show the
|
||||
'Put Back' option on some systems … This is a macOS bug."
|
||||
|
||||
→ `trash-5.2.6/src/macos/mod.rs`
|
||||
|
||||
Fehler von `trashItemAtURL` werden als `Error::Unknown` weitergereicht, nicht verschluckt.
|
||||
Abhängigkeiten: `objc2`, `objc2-foundation`, `percent-encoding` — alle drei stehen **bereits im
|
||||
`Cargo.lock`** (0.6.4 / 0.3.2 / 2.3.2), auf macOS wäre `trash` also nahezu gratis.
|
||||
|
||||
### [belegt] Linux / FreeDesktop
|
||||
|
||||
Implementiert die FreeDesktop-Trash-Spezifikation v1.0. Die Implementierung ist reines Rust, ohne
|
||||
GTK/dbus. Wichtig für die Netzlaufwerk-Frage: Der Code **legt die Trash-Ordner bei Bedarf an** und
|
||||
fällt bei einem geräteübergreifenden Verschieben explizit auf Kopieren-und-Löschen zurück:
|
||||
|
||||
```rust
|
||||
let Err(e) = std::fs::rename(src, dst) else { return Ok(()) };
|
||||
let needs_cross_device_copy = e.kind() == ErrorKind::CrossesDevices;
|
||||
if !needs_cross_device_copy { return Err((src.to_owned(), e)); }
|
||||
debug!("Cross-device move detected, falling back to copy+delete for {:?}", src);
|
||||
```
|
||||
→ `trash-5.2.6/src/freedesktop.rs`
|
||||
|
||||
**[Vermutung]** Damit ist Linux der unproblematischere Fall: Entweder die Datei landet in einem
|
||||
Trash-Verzeichnis, oder es kommt ein `Error::FileSystem` zurück. Ein stilles endgültiges Löschen
|
||||
wie unter Windows ist im Code nicht angelegt. Nicht gemessen — es stand keine Linux-Maschine zur
|
||||
Verfügung.
|
||||
|
||||
---
|
||||
|
||||
## 8. Empfehlung
|
||||
|
||||
**[Vermutung]** — das ist eine Einschätzung, keine belegte Aussage:
|
||||
|
||||
1. **`trash 5.2.6` nehmen**, mit Default-Features. Die +6 Pakete sind der Preis; verhandeln lässt
|
||||
sich daran nichts.
|
||||
2. **Den Netzlaufwerk-Fall vor der ersten Löschfunktion klären, nicht danach.** `trash` schlägt
|
||||
dort heute fehl (4.1) — die App muss diesen Fehler ohnehin abfangen und dem Nutzer erklären,
|
||||
sonst wirkt das Löschen auf Netzlaufwerken schlicht kaputt.
|
||||
3. **Eine Rückfrage vor dem Löschen ist nötig**, sobald der Workspace nicht auf einer lokalen
|
||||
Platte liegt. Der Nutzer muss wissen, ob seine Notiz wiederherstellbar ist — und aus dem
|
||||
Rückgabewert von `trash` erfährt er es nicht (5).
|
||||
4. **Nicht im UI-Thread aufrufen** (6).
|
||||
5. **Die `trash`-Version im Blick behalten**: sobald sie auf `windows` ≥ 0.61 geht, halbieren sich
|
||||
die Kosten, und `trash-rs#55` könnte bei der Gelegenheit mit erledigt werden.
|
||||
|
||||
---
|
||||
|
||||
## 9. Reproduktion
|
||||
|
||||
Die Messungen stammen aus drei Wegwerf-Programmen, die **außerhalb des Repos** gebaut wurden
|
||||
(Scratchpad, nicht eingecheckt):
|
||||
|
||||
- `main.rs` — `trash::delete` gegen lokal / Netzlaufwerk, mit Papierkorb-Gegenprobe über
|
||||
`trash::os_limited::list()`
|
||||
- `raw.rs` — `IFileOperation` direkt, mit denselben Flags, ohne `canonicalize`; zeigt zusätzlich
|
||||
das Prefix-Stripping-Verhalten
|
||||
- `sink.rs` — eigener `IFileOperationProgressSink` (`#[implement]` aus `windows 0.56`), protokolliert
|
||||
`PreDeleteItem`/`PostDeleteItem`
|
||||
|
||||
Alle angelegten Testdateien wurden wieder entfernt, die Papierkorb-Einträge mit
|
||||
`trash::os_limited::purge_all` endgültig gelöscht. Auf `Z:` sind keine Rückstände verblieben.
|
||||
|
||||
**Im Repo wurde nichts an `Cargo.toml` oder `Cargo.lock` geändert** — die Dependency-Messung lief
|
||||
über `cargo add` mit anschließendem `git checkout --`, verifiziert mit leerem
|
||||
`git status --porcelain`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Quellen
|
||||
|
||||
- crates.io-API zu `trash`: <https://crates.io/api/v1/crates/trash>
|
||||
- API-Doku: <https://docs.rs/trash/latest/trash/>
|
||||
- Quelltext `lib.rs` (Error-Enum, `canonicalize_paths`):
|
||||
<https://docs.rs/trash/5.2.6/src/trash/lib.rs.html>
|
||||
- Quelltext `windows.rs`, `freedesktop.rs`, `macos/mod.rs`, `Cargo.toml`, `README.md`,
|
||||
`CHANGELOG.md`: aus dem Crate-Tarball
|
||||
<https://static.crates.io/crates/trash/5.2.6/download>
|
||||
(auf docs.rs **nicht** einsehbar — docs.rs baut auf Linux, `windows.rs` ist dort wegcfg-t)
|
||||
- Repository (gepflegter Fork): <https://github.com/Byron/trash-rs>
|
||||
- Offener Bug zum Netzlaufwerk-Fall: <https://github.com/Byron/trash-rs/issues/55>
|
||||
- `IFileOperation::SetOperationFlags` (FOF_-/FOFX_-Flags, `FOF_WANTNUKEWARNING`):
|
||||
<https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperation-setoperationflags>
|
||||
- `IFileOperationProgressSink::PostDeleteItem` (`psiNewlyCreated` ist NULL bei endgültiger Löschung):
|
||||
<https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperationprogresssink-postdeleteitem>
|
||||
- Konstanten `FOF_NO_UI`, `TSF_DELETE_RECYCLE_IF_POSSIBLE` u. a.: generierte Bindings in
|
||||
`windows 0.56.0`, `src/Windows/Win32/UI/Shell/mod.rs`
|
||||
- FreeDesktop Trash Specification v1.0:
|
||||
<https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html>
|
||||
Reference in New Issue
Block a user