What's Changed in v3.0.4
Bug-fix release tackling eight open issues reported against v3.0.3. Focused on download correctness, webui responsiveness, storage flexibility, and analyzer coverage for new quant formats. Every fix ships with regression tests; go test -race ./... is now fully green across every package.
Bug Fixes
- Resumable downloads actually resume now (#70) — downloadSingle and each downloadMultipart part goroutine now open .part files with O_RDWR|O_CREATE (no truncate), stat the existing size, and issue Range: bytes=pos-end requests from that offset. Interrupted downloads resume correctly on rerun, and within-process retries after a flaky-connection cut no longer lose bytes. Previously every Ctrl+C silently re-fetched from zero and single-file downloads literally had no resume code path at all.
- Unsloth Dynamic quant variants correctly labeled (#72) — the GGUF quant regex now supports Q{2..8}_K_XL / _XXL suffixes and IQ1_S / IQ1_M. Previously on repos like unsloth/Qwen3-30B-A3B-GGUF the regex silently collapsed six UD-Q*_K_XL files into Q2_K..Q8_K labels that collided with the real plain-K quants, and missed the two IQ1 files entirely. All 25 GGUF files in that repo now label correctly. Added quality/description map entries for Q2_K_L, Q2..8_K_XL, IQ1_S/M, IQ2_M, IQ3_XXS.
- Multimodal GGUF repos auto-bundle the vision encoder (#76) — analyzeGGUF now partitions .gguf files into LLM quants vs mmproj vision encoders. A vision_encoder SelectableItem is emitted as Recommended by default, so the recommended CLI command for a multimodal repo becomes e.g. -F q4_k_m,mmproj-f16 for unsloth/gemma-3-4b-it-GGUF. Downloading a single quant now pulls in the matching projector automatically, matching LM Studio's behavior.
- Progress no longer oscillates on slow/flaky connections (#75) — the multipart progress ticker now stops cleanly before assembly via an explicit done channel + WaitGroup, and an explicit final full-size reading is emitted after all parts complete. Root cause: the ticker kept stating part files while assembly was deleting them, emitting downloaded=0 events that made the UI appear stuck at 2.4% ↔ 2.5% for hours on slow links. Combined with the #70 fix above, flaky downloads of multi-GB files now converge cleanly.
- Pause no longer claims the file is 100% done — fixed a regression in the #75 tail path where downloadMultipart on cancellation would fall past its errCh drain (cancelled goroutines return silently via sleepCtx without pushing errors) and emit a bogus downloaded == total event followed by assembly over an incomplete part set — corrupting the final file and deleting the very partial bytes the next resume was supposed to continue from. Now bails out with ctx.Err() immediately after the ticker shutdown. Regression test in TestDownloadMultipart_CancelMidStreamDoesNotClaim100.
- Web UI no longer floods the browser with updates (#62) — two-layer fix:
- Server-side 250ms per-job WebSocket broadcast coalescer in front of BroadcastJob. Progress events arriving inside the window collapse to a single flush of the latest state; terminal status changes (completed/failed/cancelled/paused) bypass the gate so pause/cancel transitions still feel instant.
- Frontend renderJobs no longer does container.innerHTML = jobs.map(...).join('') on every tick. Per-job DOM elements are cached and updated in place — progress bar width and stats text change, but the card node, the action buttons, and their event listeners stay stable. Hover states persist and Pause/Cancel buttons are clickable during an active download.
- Dismissed jobs stay dismissed across refresh (#68) — new POST /api/jobs/{id}/dismiss endpoint permanently removes a terminal-state job from the manager. Frontend dismissJob now calls the server before removing from local state, so page reloads and WebSocket reconnects don't repopulate it. Attempts to dismiss queued or running jobs return 409. The primary per-file-deletion ask in the same issue is tracked separately.
- JobManager returns snapshots — data race fixed — CreateJob, GetJob, ListJobs, and the internal WebSocket broadcast path all now return/forward cloned Job snapshots via a new cloneJobLocked helper. Previously the HTTP JSON encoder and the WS broadcaster would read Job fields while runJob was mutating them on a separate goroutine. go test -race ./... is now fully green across every package for the first time.
Features
- --local-dir CLI flag (#71, #73) — new flag mirroring huggingface-cli download --local-dir. Downloads real files into the chosen directory instead of the HF cache's blobs+symlinks layout. Right choice for feeding weights to llama.cpp / ollama, Windows users without Developer Mode, and NFS/SMB/USB transfers. Equivalent to the existing --legacy -o form — both spellings are permanent and interchangeable, and --legacy is no longer marked for removal.
- Installer defaults to ~/.local/bin — no more sudo prompt (#69) — the one-liner bash <(curl -sSL https://g.bodaay.io/hfd) install now picks a user-local install path in this order:
- ~/.local/bin if already in PATH
- ~/bin if already in PATH
- /usr/local/bin if writable
- Fallback to ~/.local/bin with a printed export PATH= line
Explicit targets like install /usr/local/bin still work and still use sudo where needed. Root users still get /usr/local/bin by default.
Documentation
- README now has a prominent Storage Modes section documenting both HF-cache-default and flat-file --local-dir modes as first-class, permanent options with when-to-use-which guidance.
- docs/CLI.md and docs/V3_FEATURES.md updated to reflect the un-deprecated --legacy / --output flags and the new --local-dir spelling.
Test Infrastructure
- TestAPI_Health no longer pins to a stale hardcoded version string.
- TestJobManager_CreateJob no longer races its TempDir cleanup against in-flight runJob goroutines — new JobManager.WaitAll(timeout) lets the test block on actual goroutine exit before cleanup runs.
Full Changelog: https://github.com/bodaay/HuggingFaceModelDownloader/compare/v3.0.3...v3.0.4