Repository navigation
feat(desktop): download ffmpeg, ffprobe and wtsexporter into the Tools Directory at start (#1053) - #2001
Conversation
…s Directory at start (#1053) At every start the desktop app checks the Tools Directory on a thread of its own and downloads what is missing or older than the pinned release: ffmpeg and ffprobe from eugeneware/ffmpeg-static b6.1.1 (the .gz files, unless both are on PATH), and wtsexporter from messagecrate/WhatsApp-Chat-Exporter 0.13.0-mc.2. Each file is checked against a SHA-256 the app carries; one that does not match is deleted. A new file is written under a temporary name and renamed over the old one only after it passed, so a failed download leaves the old one in use. tools/manifest.json records the release, asset, checksum and size of each file the app wrote, so a start reads no program again. ffmpeg or ffprobe the app did not write is left alone while it answers -version; wtsexporter the app did not write is kept only when its SHA-256 is the pinned one. tools_status gains the downloading and downloadFailed states, and Settings -> System -> Media shows them, asking again each second while a download runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
mbeisser1
left a comment
There was a problem hiding this comment.
Review of 485d442 (up to date with main). Findings whose line is outside the diff follow; the rest are line comments.
docs/src/content/docs/docs/user/import-sources/whatsapp.md— Standards · rule —whatsapp.md:11,26-38is outside the diff and now describes the code wrongly: it says wtsexporter "must be installed separately" and tells the reader to link apipxinstall into the Tools Directory. Under this PR's rule (a hand-placed wtsexporter is replaced unless it is the pinned file,system.md:62) apipxlink never matches the pinned checksum, so the app replaces it at every start.troubleshooting.md:99("which the desktop app doesn't include") andattachments-and-media.md:76-81also predate the download. Step 5 rewrites these pages, but a page that now tells the reader to do something the app undoes is a broken instruction: cut the pipx step to a one-line note that the app downloads wtsexporter, or make the pages true in the minimal way.docs/adr/0019-the-desktop-app-downloads-the-programs-it-needs.md:84 — Standards · rule — The ADR says a newer pin "replaces what is there, whoever put it there". The code (need,tool_downloads.rsaround line 380) andsystem.md:59keep a hand-placed ffmpeg or ffprobe while it runs. The ADR and the code disagree and the PR changes neither to match the other. (Spec reports the same rule as reversed; the fix is in the code, see that thread.)
…endment The decision text is back as it was on main. The status line says the download was built in #2001 and that the app records what it wrote beside the programs. A dated amendment says wtsexporter comes from Message Crate's fork and why. The glossary names a record beside the files, not its file name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…nloads/pins.rs A newer pinned release is now an edit to one file, which holds the release names, the per-platform asset tables and the checksums with where each came from. The comment on the ffmpeg release says what the file is: the b6.1.1 release of ffmpeg-static, whose files report ffmpeg 7.0.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…r is the lookup itself ffmpeg_on_path and resolve_tools both call searched_path instead of each working out the search path. tool_in_dir no longer forwards to find_tool_in_dir: the lookup is public under that name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
A file in the Tools Directory is kept only when the record names it with the pinned release, its stamp (size, modified time and, on Unix, inode) is the one recorded, and it runs. A file with no entry whose SHA-256 is the pinned one is adopted. Anything else under a program's name is replaced after a good download, whoever put it there, as #1053 and ADR 0019 say; a hand-placed ffmpeg that answers -version is no longer kept for good. The old file stays in use until the new one passed its checksum, so with no internet it is still the one used. A recorded file that does not run is downloaded again, and a download that does not run either fails with "The file was downloaded but does not run" instead of leaving the row at missing with no reason. The record keeps the stamp in place of the size alone, so a person's file of the same size is not taken for the app's. Settings → System says the one rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… left An app closed or killed mid-download runs no clean-up, so the temporary files under the Tools Directory stayed, up to about 110 MB each time. The check now deletes files with its own temporary prefixes before it starts, and the prefixes are built in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ailed line download_missing fails every wanted program through one closure instead of two copies of the same loop. The four download failures no longer differ in whether they say they are retried: none of the reasons says it, and the failed line in Settings says it once for all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… start The state was set only once a download's response headers arrived, so Settings opened during the connect showed missing and never asked again, and a poll in the gap between ffmpeg and the next program stopped asking and missed the final found. Each program the check will download is now downloading, 0 bytes of an unknown total, before the first request, and Settings asks again while any program is downloading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
with_download turned Unusable plus a failed download into DownloadFailed, which hid "found in two places", the cause a person must fix. A failed download now replaces only a missing program's line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ames It failed only on the intended change, a release bump. It still checks that each platform pins the programs it should, that each checksum is a whole SHA-256, and the shape of each release address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
queryKeys.ts says every cache key is built there and nowhere else. The desktop tools status key moves there, with a note that it is this computer's and so carries no account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The entry named a file path, PATH, two repository slugs and their release tags. It now says each program comes from a pinned release of its project, checked against a checksum the app carries, and that Settings → System shows progress and failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… downloads the programs The WhatsApp page told the reader to install wtsexporter with pipx and link it into the Tools Directory, which the app now replaces with the pinned file at every start. It now says the app downloads wtsexporter into its Tools Directory and Settings → System shows it. The troubleshooting entry and the attachments page no longer say the app doesn't include the programs. Step 5 of #1053 rewrites these pages in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The pins for ffmpeg and ffprobe are of the `.gz`, so a file in the Tools Directory could never match one, and a lost record or a stamp that stopped matching downloaded them again at every start. Each pin now carries the unpacked program's SHA-256 too, the record keeps it, and a file with no entry or a changed stamp is adopted when its SHA-256 is the pinned program's. The download checks the unpacked program against it. The ten unpacked hashes come from `curl -L` of each b6.1.1 asset, `gunzip -c` and `sha256sum`, on 2026-10-08; the `.gz` hashes matched the pins already carried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
A recorded file of the pinned release that did not run, or the pinned program put there by hand that did not run, was downloaded again at every start, for good, and each download gave the same file. The check now keeps it and says it doesn't run without downloading. A newer pin, or a changed file, still starts a download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
"The file was downloaded but does not run." named no program and left nothing to do. ffmpeg and ffprobe now say to install the program with a package manager, because the app uses the copy on PATH. wtsexporter is never looked for on PATH, so its line points to Troubleshooting instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
Two copies of the app open at once each ran the check: the second's sweep of leftover temporary files deleted the first's download in progress, and both wrote the record. The check now holds an exclusive lock on `.check.lock` in the Tools Directory while it runs, and a check that can't take it returns at once, because the first does the work. The lock is std's `File::try_lock`, so no crate is added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ated Settings → System named `b6.1.1` and `0.13.0-mc.2`, and the doc text in pins.rs repeated `b6.1.1`, so "a newer pinned release is a change to this file alone" was untrue. The user page now says "a pinned release", and the doc text names no tag. The wtsexporter tag is still in the install hints of the WhatsApp exporter and in the developer install, so the comment now names those places for a newer wtsexporter release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
System listed which files are replaced, including the app's own file when it doesn't run, and WhatsApp said a copy put there by hand is replaced. Both now say the same sentence: a program whose checksum is the pinned program's is kept, whoever put it there, and anything else is replaced. System also says a program that doesn't run is not downloaded again, and that ffmpeg's and ffprobe's reason says to install them with a package manager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The note ended on a link to the Homebrew copies being replaced, which read as something to do. It now says the app opened from the Dock doesn't see Homebrew's PATH, downloads its own copies into the Tools Directory, and that nothing needs doing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…it there The page quoted the review telling a person to put ffprobe in the Tools Directory, which the one Tools Directory rule makes stale: the app downloads both programs there at start. The page now says so, and that the package manager commands above install them when the download fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
"Since 2026-10-02" named no change, unlike the other amendments, which name the pull request. The wtsexporter the app downloads is the fork's from #2001, the pull request that adds the download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… by how it got there The reason said "was downloaded", which is untrue for a copy put in the Tools Directory by hand whose hash matches the pin. Both cases now read "<program> in the Tools Directory doesn't run on this computer". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
mbeisser1
left a comment
There was a problem hiding this comment.
Re-review of the fix commits (Standards and Correctness, step 4 of the review), pinned to d8f8571. Every finding below is fixed in this push.
Correctness · bug src-tauri/src/tool_downloads.rs:276: a program that downloads but does not run was recorded and downloaded again at every start, for good. Fixed in 16d6ecb: a recorded pinned program that doesn't run shows the failure without downloading; a new pin or a changed stamp starts a download. Test: a second start downloads nothing.
Correctness · bug tool_downloads.rs:621: ffmpeg and ffprobe could never be adopted (their pins are the .gz hashes), so a lost record or a changed stamp re-downloaded them at every start. Fixed in d017884: each pin and the record carry the unpacked program's SHA-256 (computed from the ten b6.1.1 assets with curl -L, gunzip -c, sha256sum); a file with no entry or a changed stamp is adopted when its hash matches; the download also checks the unpacked file. Tests for adoption, re-recording and an unpacked mismatch.
Correctness · bug tool_downloads.rs:87: two app instances deleted each other's temp files and raced on the record. Fixed in fe103ef: the check holds an exclusive lock on .check.lock in the Tools Directory (std File::try_lock); a second check returns at once; a filesystem without locks runs unlocked. Two-thread test.
Correctness · nit tool_downloads.rs:398,404: "was downloaded but doesn't run" was untrue for an adopted hand-placed file. Fixed in d8f8571: " in the Tools Directory doesn't run on this computer".
Standards · judgement system.md:59-60, whatsapp.md:18: the "one rule" overstated adoption and the two pages disagreed. Fixed in 25756e0 (after d017884 the rule is one rule for all three programs).
Standards · rule pins.rs:2-3: "a change to this file alone" was untrue. Fixed in 220fc4a: the user page and the doc text no longer name the tags; the comment names the places a wtsexporter release name still appears (the install hints, AGENTS.md, contributing.md).
Standards · judgement tool_downloads.rs:374: the reason had no contraction and said nothing a person can do. Fixed in 5532870 and d8f8571: ffmpeg and ffprobe point at the package manager (the app uses a copy on PATH), wtsexporter at Troubleshooting.
Standards · judgement system.md:81: the Homebrew sentence. Fixed in 04f63d6.
Standards · judgement attachments-and-media.md:86: stale "Put it in the Tools Directory" advice. Fixed in 36fef7d.
Standards · judgement ADR 0019:92: "Since 2026-10-02". Fixed in 9698bd2: "Since #2001".
|
Review summary for d8f8571.
|
Part of #1053 (step 3). It does not close the issue: step 4 (the Import form's message, Try again, and the wait for a download) and step 5 (the user guide) follow.
Feature Description
The desktop app downloads ffmpeg, ffprobe and wtsexporter into the Tools Directory (
~/message-crate/tools) by itself. Each time it starts, a background thread checks the Tools Directory and downloads what is missing or older than the pinned release. Login and browsing don't wait. With no internet the app starts and works, the failure is shown in Settings → System → Media, and the download is tried again at the next start.eugeneware/ffmpeg-staticreleaseb6.1.1. Neither is downloaded when both are onPATH(the step-2 lookup).messagecrate/WhatsApp-Chat-Exporterrelease0.13.0-mc.2. It is never looked for onPATH.User Story
As a person who installs the desktop app on a computer without ffmpeg or wtsexporter, I want the app to fetch them by itself, so that a WhatsApp import and Convert work with nothing else installed and nothing clicked.
Implementation Details
The record of what the app wrote.
tools/manifest.jsonholds, per program, the release, asset, SHA-256 and the size of the file written. A file whose size matches its entry is the app's. It is current when the entry names the pinned release, so no program is read again at start-up, and a newer pin replaces it. A file with no entry, or whose size changed, was put there by someone else:-version(media::tool_in_dir) is left in place, such as one linked in from Homebrew. One that doesn't run is downloaded over.The source. Each pinned file's address is
{base}/{repo}/releases/download/{tag}/{asset}.baseishttps://github.comin the app, and the tests point it at anhttpmockserver, whichsrc-taurialready has as a dev-dependency.The asset choice. The ffmpeg-static
.gzfiles are downloaded rather than the plain ones, because they are a third of the size (19 to 30 MB against 45 to 83 MB). The checksum is over the.gzas it arrives, before it is unpacked withflate2. Windows on ARM gets the x64 ffmpeg files; whether they run there is unchecked, as the issue says. Linux on ARM gets no wtsexporter, because the fork publishes none for it.The checksums. ffmpeg-static publishes no checksum file. Each
b6.1.1.gzasset for linux-x64, linux-arm64, darwin-x64, darwin-arm64 and win32-x64 was downloaded withcurl -Lon 2026-10-08 and hashed withsha256sum, andgzip -tpassed on each. The wtsexporter checksums are copied from the release'sSHA256SUMSasset, read on 2026-10-08. The app never fetchesSHA256SUMS. All are constants insrc-tauri/src/tool_downloads.rs, with a comment saying how they were made.The HTTP client.
reqwest0.12 blocking, with the workspace's rustls features. The connect timeout is 30 seconds, and a 60-second timeout covers each read, so a stalled download fails rather than hanging. TheUser-AgentisMessageCrate/<build> (desktop app tool download).The status.
tools_status(tagged bystate) gainsdownloading(received, andtotalornull) anddownloadFailed(reason: no connection, the HTTP status with its reason phrase, checksum mismatch, or could not write). A download in progress is shown over whatever was found. A failed one is shown only when the program is not found, because a program found is still the one used. The Settings screen reads the status through TanStack Query (useQuery, keyed["desktop", "tools-status"], not account-scoped because the programs belong to the computer) with arefetchIntervalof one second while any program is downloading.Checked by hand. A run of the downloader against the real GitHub releases for linux-x64, not committed, downloaded all three files, passed every checksum and wrote the record. The ffmpeg that arrived answered
-version: theb6.1.1linux-x64 file reportsffmpeg version 7.0.2-static(johnvansickle.com), not 6.1.1, which a reviewer may want to know. wtsexporter answered--help.The GPL rule (ADR 0014) is not touched. The app downloads a file to the person's computer and links nothing.
macOS quarantine is unchecked. The app doesn't try to clear it, and whether a downloaded program runs on macOS without a security prompt is still unchecked, as #1053 says.
Key Files Changed
src-tauri/src/tool_downloads.rs(new) - the pinned releases and checksums, the record, the decision per program, the streaming download with its checksum, the replace-after-success, and the background start.src-tauri/src/tool_downloads/tests.rs(new) - the checksum refusal, the replace-after-success, ffmpeg onPATH, hand-placed files, no network, and the platform table.src-tauri/src/commands/tools.rs-downloadinganddownloadFailedstates,tools_statusreads the sharedToolDownloads.src-tauri/src/main.rs- managesToolDownloadsand starts the check after the Tools Directory is named.crates/libs/media/src/tools.rs-ffmpeg_on_path()andtool_in_dir();testutil::locate_toolsfor tests.web/src/screens/settings/SystemSection.tsx,web/src/lib/tauri.ts- the two new status lines, anduseQuerywith polling while a download runs.docs/.../settings/system.md,CONTEXT.md,docs/adr/0019- the download is built; ADR 0019 names the fork as the wtsexporter source, and why.HTTP API Changes
tools_status, which gains two states.Database Changes
schema/sql/*.sqlchanged (every database rebuilds empty and re-imports; the fingerprint is computed, nothing to bump)How to Test
cargo test --manifest-path src-tauri/Cargo.toml toolruns the downloader tests against a localhttpmockserver.cd web && npx vitest run src/screens/settings/SystemSection.test.tsxcovers the in-progress and failed lines and the polling.PATH, empty~/message-crate/toolsand start the app. Settings → System → Media shows the download's progress, then the three programs found.tools/manifest.jsonnames the releases.download failedwith no connection as the reason.Checklist
./scripts/check-pr.shpasses and formatter rewrites are committedAlso run:
cargo test --manifest-path src-tauri/Cargo.toml(182 passed),cargo test -p media,cd web && npx vitest run(2134 passed),cd docs && npx astro check(0 errors).Related Issues
Part of #1053 (step 3).
Screenshots / Demo
No browser check. The status lines appear only in the desktop app, and the Vitest tests cover the in-progress and failed rows.
Deployment Notes
New
src-tauridependencies:reqwest0.12 (blocking, rustls; already in the workspace lockfile),sha20.11,hex0.4,flate21.1. Not tested: the download on macOS and Windows, and the rename over a running.exeon Windows, which would fail as "could not write" and be retried at the next start.🤖 Generated with Claude Code
https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1