Webexport meldt 'geëxporteerd naar' ook als de browserdownload nooit startte #1902

Closed
opened 2026-08-31 22:13:44 +00:00 by brenno · 2 comments
Owner

Probleem

Op web is elke uitgang een browserdownload — er is geen bestandssysteem. Elke
downloadroute die via FilePicker.saveFile loopt meldt succes zodra er geen
exception viel. Maar FilePicker.saveFile geeft op web altijd null terug,
ook wanneer het goed ging (file_picker_web 3.0.1,
lib/src/file_picker_web.dart:238-278: Blob + anker + click(), dan
return null). Er is dus geen signaal dat de download werkelijk startte — en
er valt er ook geen te krijgen: anchor.click() gooit niets wanneer de browser
de download tegenhoudt.

Gevolg: de gebruiker leest "Pakket geëxporteerd naar: deck.ocideck" terwijl er
niets in de downloadmap staat.

Waar het staat

Plek Wat er gebeurt
lib/services/export_service.dart:251-262 PDF/PPTX/ODP/HTML/LaTeX plus het redactiemanifest; return ExportResult.ok(fileName) staat onvoorwaardelijk ná de saveFile-aanroepen
lib/services/document_export_service.dart:422-427 return webFileName; idem
lib/services/file/file_service_package.dart:53-58 (downloadPackage) return name; idem
lib/services/file/file_service_dossier.dart:72-88 (downloadDossier) return name; idem
lib/widgets/shell/shell_actions_export.dart:51 en :120 de melding: "Pakket geëxporteerd naar: " — desktopbewoording voor iets wat misschien niet gebeurd is

Ter vergelijking, en om de reikwijdte eerlijk te houden: de eigen
downloadTextFile (lib/utils/file_download_web.dart) geeft wel een bool
terug, en _saveAsDownload (lib/state/deck_provider.dart:354) en
lib/widgets/presentation/session_export.dart:307 kijken daar ook naar. Maar
die bool betekent alleen "er viel geen exception"; hij ziet net zomin of het
bestand ergens aankwam. Beide routes hebben dus dezelfde blinde vlek — de
FilePicker-route heeft er alleen niet eens een bool bij.

De concrete manier waarop het misgaat

Browsers laten de eerste automatische download door en zetten een poort voor de
tweede en verder (Chrome vraagt "meerdere bestanden downloaden?"; geweigerd of
geblokkeerd is stilzwijgend niets). Twee plekken vuren meer dan één download
achter elkaar af:

  1. Een geredigeerde export levert er twee tot drie: het rapport, het
    …-redactie.json-manifest, en bij salts de verificatiesleutels
    (_redactionManifestFiles, export_service.dart:475-493). Stopt de browser
    na de eerste, dan houdt de auteur een geredigeerd rapport in handen en
    gelooft hij dat commitments én sleutels mee zijn — precies wat de comment op
    export_service.dart:255-258 wilde voorkomen.
  2. De sessie-export schrijft per dia een los bestand
    (session_export.dart:302-308).

Op desktop is die volgorde met zorg geregeld — manifest eerst, dan het grote
bestand, met de reden erbij (export_service.dart:267-272). Op web valt die
zorg weg zonder dat iemand het merkt.

Wat vaststaat en wat niet

Vast, uit de code: saveFile geeft op web altijd null, en alle vier de
routes melden onvoorwaardelijk succes. De multi-download-poort van de browser
is de aannemelijke aanleiding, maar is niet in een echte browser nagemeten
dat hoort bij de repro te gebeuren vóór de reparatie.

Voorgestelde oplossing

  1. Zeg wat we weten. Op web niet "geëxporteerd naar " maar "aangeboden
    als download: ". Klein, en hoe dan ook waar.
  2. Stop met één-voor-één downloaden — dit is de echte reparatie. Een export
    die uit meer dan één bestand bestaat, gaat op web als één ZIP de deur uit.
    Eén download, geen browserpoort, en het manifest kan niet los zoekraken.
    Raakt de vraag hoe de ontvanger uitpakt, dus beslis dit vóór punt 1.
  3. Maak de exceptietak eerlijk. Roep op web niet FilePicker.saveFile aan
    maar de eigen Blob+anker uit file_download_web.dart, achter een
    injecteerbare seam. Verwacht daar niet meer van dan het vangen én loggen van
    exceptions; een door de browser geblokkeerde download blijft voor de pagina
    onzichtbaar.

Regressietest

De test die dit had moeten vangen bestaat niet, en kan nu ook niet bestaan:
FilePicker.saveFile wordt statisch aangeroepen, er is niets om tegen te
meten. Die seam is meteen het echte werk.

  • Een web-export met een niet-leeg redactiemanifest doet ná de ZIP-reparatie
    precies één download-aanroep, niet drie.
  • Faalt de downloadfunctie, dan komt er géén ExportResult.ok uit — nu is dat
    onmogelijk te toetsen.

Repro

  1. Open de webversie met een deck dat redacties bevat.
  2. Zet in Chrome voor die site Automatische downloads op Blokkeren
    (slotje → Site-instellingen).
  3. Exporteer naar PDF.
  4. Verwacht: een melding dat niet alles is geleverd.
  5. Krijgt: "Geëxporteerd naar …", terwijl het manifest en de sleutels
    ontbreken.

Herkomst

Gevonden bij het nalopen van de downloadroutes van de webbuild, naast #1720
(dat de ontbrekende kIsWeb-tak in de documentexport repareerde — die tak is
er nu, maar meldt succes op dezelfde onvoorwaardelijke manier).

Labels

bug

## Probleem Op web is elke uitgang een browserdownload — er is geen bestandssysteem. Elke downloadroute die via `FilePicker.saveFile` loopt meldt succes zodra er geen exception viel. Maar `FilePicker.saveFile` geeft op web **altijd `null`** terug, ook wanneer het goed ging (`file_picker_web` 3.0.1, `lib/src/file_picker_web.dart:238-278`: Blob + anker + `click()`, dan `return null`). Er is dus geen signaal dat de download werkelijk startte — en er valt er ook geen te krijgen: `anchor.click()` gooit niets wanneer de browser de download tegenhoudt. Gevolg: de gebruiker leest "Pakket geëxporteerd naar: deck.ocideck" terwijl er niets in de downloadmap staat. ## Waar het staat | Plek | Wat er gebeurt | |---|---| | `lib/services/export_service.dart:251-262` | PDF/PPTX/ODP/HTML/LaTeX plus het redactiemanifest; `return ExportResult.ok(fileName)` staat onvoorwaardelijk ná de `saveFile`-aanroepen | | `lib/services/document_export_service.dart:422-427` | `return webFileName;` idem | | `lib/services/file/file_service_package.dart:53-58` (`downloadPackage`) | `return name;` idem | | `lib/services/file/file_service_dossier.dart:72-88` (`downloadDossier`) | `return name;` idem | | `lib/widgets/shell/shell_actions_export.dart:51` en `:120` | de melding: "Pakket geëxporteerd naar: <naam>" — desktopbewoording voor iets wat misschien niet gebeurd is | Ter vergelijking, en om de reikwijdte eerlijk te houden: de eigen `downloadTextFile` (`lib/utils/file_download_web.dart`) *geeft* wel een bool terug, en `_saveAsDownload` (`lib/state/deck_provider.dart:354`) en `lib/widgets/presentation/session_export.dart:307` kijken daar ook naar. Maar die bool betekent alleen "er viel geen exception"; hij ziet net zomin of het bestand ergens aankwam. Beide routes hebben dus dezelfde blinde vlek — de `FilePicker`-route heeft er alleen niet eens een bool bij. ## De concrete manier waarop het misgaat Browsers laten de eerste automatische download door en zetten een poort voor de tweede en verder (Chrome vraagt "meerdere bestanden downloaden?"; geweigerd of geblokkeerd is stilzwijgend niets). Twee plekken vuren meer dan één download achter elkaar af: 1. **Een geredigeerde export levert er twee tot drie**: het rapport, het `…-redactie.json`-manifest, en bij salts de verificatiesleutels (`_redactionManifestFiles`, `export_service.dart:475-493`). Stopt de browser na de eerste, dan houdt de auteur een geredigeerd rapport in handen en gelooft hij dat commitments én sleutels mee zijn — precies wat de comment op `export_service.dart:255-258` wilde voorkomen. 2. **De sessie-export** schrijft per dia een los bestand (`session_export.dart:302-308`). Op desktop is die volgorde met zorg geregeld — manifest eerst, dan het grote bestand, met de reden erbij (`export_service.dart:267-272`). Op web valt die zorg weg zonder dat iemand het merkt. ## Wat vaststaat en wat niet Vast, uit de code: `saveFile` geeft op web altijd `null`, en alle vier de routes melden onvoorwaardelijk succes. De multi-download-poort van de browser is de aannemelijke aanleiding, maar is **niet in een echte browser nagemeten** — dat hoort bij de repro te gebeuren vóór de reparatie. ## Voorgestelde oplossing 1. **Zeg wat we weten.** Op web niet "geëxporteerd naar <pad>" maar "aangeboden als download: <naam>". Klein, en hoe dan ook waar. 2. **Stop met één-voor-één downloaden** — dit is de echte reparatie. Een export die uit meer dan één bestand bestaat, gaat op web als één ZIP de deur uit. Eén download, geen browserpoort, en het manifest kan niet los zoekraken. Raakt de vraag hoe de ontvanger uitpakt, dus beslis dit vóór punt 1. 3. **Maak de exceptietak eerlijk.** Roep op web niet `FilePicker.saveFile` aan maar de eigen Blob+anker uit `file_download_web.dart`, achter een injecteerbare seam. Verwacht daar niet meer van dan het vangen én loggen van exceptions; een door de browser geblokkeerde download blijft voor de pagina onzichtbaar. ## Regressietest De test die dit had moeten vangen bestaat niet, en kan nu ook niet bestaan: `FilePicker.saveFile` wordt statisch aangeroepen, er is niets om tegen te meten. Die seam is meteen het echte werk. - Een web-export met een niet-leeg redactiemanifest doet ná de ZIP-reparatie precies één download-aanroep, niet drie. - Faalt de downloadfunctie, dan komt er géén `ExportResult.ok` uit — nu is dat onmogelijk te toetsen. ## Repro 1. Open de webversie met een deck dat redacties bevat. 2. Zet in Chrome voor die site *Automatische downloads* op Blokkeren (slotje → Site-instellingen). 3. Exporteer naar PDF. 4. Verwacht: een melding dat niet alles is geleverd. 5. Krijgt: "Geëxporteerd naar …", terwijl het manifest en de sleutels ontbreken. ## Herkomst Gevonden bij het nalopen van de downloadroutes van de webbuild, naast #1720 (dat de ontbrekende `kIsWeb`-tak in de documentexport repareerde — die tak is er nu, maar meldt succes op dezelfde onvoorwaardelijke manier). ## Labels bug
Author
Owner

Opgepakt. Tak: fix/webdownload-succesmelding-1902. Verwachte reikwijdte: lib/utils/file_download*.dart (seam + bytes-download), lib/services/export_service.dart, lib/services/document_export_service.dart, lib/services/file/file_service_package.dart, lib/services/file/file_service_dossier.dart, lib/widgets/presentation/session_export.dart, lib/widgets/shell/shell_actions_export.dart, plus tests en l10n.

Opgepakt. Tak: fix/webdownload-succesmelding-1902. Verwachte reikwijdte: lib/utils/file_download*.dart (seam + bytes-download), lib/services/export_service.dart, lib/services/document_export_service.dart, lib/services/file/file_service_package.dart, lib/services/file/file_service_dossier.dart, lib/widgets/presentation/session_export.dart, lib/widgets/shell/shell_actions_export.dart, plus tests en l10n.
Author
Owner

Opgelost en op main: 1948e97c2 (PR #1906).

Wat er nu gebeurt. Eén export vertrekt als één download. Bestaat hij uit meer
dan één bestand — een geredigeerd rapport met zijn manifest en sleutels, een
sessie met een bestand per dia — dan gaat het als één ZIP, genoemd naar het
hoofdbestand plus .zip. Eén bestand gaat nog steeds als zichzelf. De webversie
meldt aangeboden als download in plaats van geëxporteerd naar, want de pagina
kan niet zien of het bestand in de downloadmap aankwam. Een geweigerde download
levert een echte fout op in plaats van een bestandsnaam die nergens staat.

Meer dan de vier paden uit de melding. Het stijlprofiel en het RFC
3161-tijdstempelverzoek deden hetzelfde. Die tweede was de vervelendste: hij
bewaarde de nonce ná de export, terwijl de comment daar met zoveel woorden zegt
dat dat pas mag als het verzoek de deur uit is. In lib/ staat nu geen enkele
FilePicker.saveFile-aanroep meer.

Eén ding erbij dat hier niet in stond. De conditional import koos onder
dart2wasm stilzwijgend de lege stub — dart.library.html is daar onwaar. Op een
wasm-build zou élke download een lege huls zijn geweest. Nagemeten in de
glue-module van een echte --wasm-build: met dart.library.html staat
.download er 0× in en revokeObjectURL 1×, met dart.library.js_interop 1× en
2×. Staat nu op js_interop, zoals clipboard_html.dart en
presenter_fullscreen.dart al deden. Relevant voor #1734.

Toetsing. Zeventien toetsen erbij. De hele webtak lag buiten bereik van de
suite (kIsWeb is op de VM altijd false); twee @visibleForTesting-haken in
download_delivery.dart maken hem meetbaar. Rood geproefd door de afleverlaag
terug te zetten op het oude gedrag: vier toetsen vallen dan om, op precies de
twee claims uit dit issue. make check groen, plus make build-web en
flutter build web --wasm.

Wat níet is nagemeten, zoals hierboven al stond: dat de
multi-download-poort van de browser dit werkelijk triggert, is niet in een echte
browser bevestigd. De code-analyse staat vast en de reparatie is hoe dan ook de
goede kant op — één download kan die poort niet raken — maar de repro-stappen in
dit issue zijn nog steeds de manier om het met eigen ogen te zien.

Opgelost en op `main`: 1948e97c2 (PR #1906). **Wat er nu gebeurt.** Eén export vertrekt als één download. Bestaat hij uit meer dan één bestand — een geredigeerd rapport met zijn manifest en sleutels, een sessie met een bestand per dia — dan gaat het als één ZIP, genoemd naar het hoofdbestand plus `.zip`. Eén bestand gaat nog steeds als zichzelf. De webversie meldt *aangeboden als download* in plaats van *geëxporteerd naar*, want de pagina kan niet zien of het bestand in de downloadmap aankwam. Een geweigerde download levert een echte fout op in plaats van een bestandsnaam die nergens staat. **Meer dan de vier paden uit de melding.** Het stijlprofiel en het RFC 3161-tijdstempelverzoek deden hetzelfde. Die tweede was de vervelendste: hij bewaarde de nonce ná de export, terwijl de comment daar met zoveel woorden zegt dat dat pas mag als het verzoek de deur uit is. In `lib/` staat nu geen enkele `FilePicker.saveFile`-aanroep meer. **Eén ding erbij dat hier niet in stond.** De conditional import koos onder dart2wasm stilzwijgend de lege stub — `dart.library.html` is daar onwaar. Op een wasm-build zou élke download een lege huls zijn geweest. Nagemeten in de glue-module van een echte `--wasm`-build: met `dart.library.html` staat `.download` er 0× in en `revokeObjectURL` 1×, met `dart.library.js_interop` 1× en 2×. Staat nu op `js_interop`, zoals `clipboard_html.dart` en `presenter_fullscreen.dart` al deden. Relevant voor #1734. **Toetsing.** Zeventien toetsen erbij. De hele webtak lag buiten bereik van de suite (`kIsWeb` is op de VM altijd `false`); twee `@visibleForTesting`-haken in `download_delivery.dart` maken hem meetbaar. Rood geproefd door de afleverlaag terug te zetten op het oude gedrag: vier toetsen vallen dan om, op precies de twee claims uit dit issue. `make check` groen, plus `make build-web` en `flutter build web --wasm`. **Wat níet is nagemeten**, zoals hierboven al stond: dat de multi-download-poort van de browser dit werkelijk triggert, is niet in een echte browser bevestigd. De code-analyse staat vast en de reparatie is hoe dan ook de goede kant op — één download kan die poort niet raken — maar de repro-stappen in dit issue zijn nog steeds de manier om het met eigen ogen te zien.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1902
No description provided.