A truncated debrid download (in-memory byte counter hit 100% while the
NFS write-back silently dropped most of the bytes) was marked completed.
The 1.1.6 fsync fix closed the debrid-specific hole; this generalizes the
guarantee so "completed" never means a corrupt file on ANY backend.
- IntegrityError + bounded retry: on a corrupt/short result the manager
re-downloads the same source up to 3x (clean start), then surfaces the
task as damaged ("corrupt download:" prefix) instead of completing it.
- verify (size mismatch / empty), debrid (incomplete / post-write / flush),
torrent (BytesMissing), usenet (par2 unrepairable / repair-failed) all
classify integrity failures so they route through the retry/damaged path.
- scanner: a file ffprobe can't read is emitted as a damaged library_item
(reason "unreadable") instead of being silently dropped from the sync.
- tests: manager retry-then-success + retry-exhausted-then-damaged,
verifying->resolving transition, damaged sync item.
Symmetric hardening with the debrid fix (2026-06-15 NFS write-back race):
the prod download dir is a network mount and usenet post-processing reads
files back for par2 from the page cache while the write-back to the server
can still lag. A later open (organize/stream/ffprobe) would then see a
short file. fsync the delivered file (or every regular file in a multi-file
release directory) and surface any write-back error before returning.
Also report the real content size for directory deliveries (walk instead
of the dir inode size) and fail an empty delivery.
Add syncTree/syncFile + tests.
- usenet: Par2Verify/Repair return ErrPar2NotInstalled (was nil="verified");
pipeline surfaces it via Result.VerifyNote + WARNING — a download that
shipped parity but couldn't be checked is delivered UNVERIFIED, not verified.
- funnel: pin cloudflared version + verify a baked-in SHA-256 (was `latest` +
ELF-magic only) — a malicious/broken upstream release isn't pulled silently.
- stream: makeReadable verifies the file actually opens after chmod and warns
clearly (NFS root_squash / SMB uid mapping) instead of a cryptic later EPERM.
- WireGuard endpoint pin dropped from the debt list (reseller uses direct
config, no pin).
CheckDiskSpace (internal/engine/diskspace.go) refuses a download before
writing when its expected size wouldn't leave a configurable reserve free,
so a download never fills the filesystem to 0 mid-write (which corrupts the
partial file). Wired into all three downloaders ahead of any write — torrent
(DataDir), debrid (outputDir, resume-aware), usenet (outputDir, fresh only).
Reserve from downloads.min_free_disk_mb (default 2048 MiB) via SetMinFreeBytes.
The manager treats an InsufficientDiskError as terminal — no source fallback,
since another source would fill the same disk — and surfaces the clear message.
Best-effort: unknown size or a stat failure doesn't block (ENOSPC stays the
backstop). Also hardens formatBytes against an exabyte-scale out-of-bounds panic.
- task.go: fix deadlock in ToStatusUpdate() — calling Percent() (which
RLocks) while already holding RLock caused deadlock when a writer was
waiting; compute percent inline instead
- usenet.go: fix data race in Cancel() — tracker and taskDir were read
without the mutex while Download() writes them under it; read all
fields under the same lock
- upnp.go: fix UPnP Remove() blocking shutdown — run cleanup in goroutine
with 10s deadline (removeNATPMP worst case is 3s dial + 5s deadline)
- daemon.go: add path traversal protection for stream requests — validate
sr.FilePath is within configured directories before os.Stat; defends
against compromised API server sending arbitrary paths
- client.go: add wakeClient without timeout for long-poll wake endpoint
where context controls cancellation
- sync.go: trigger immediate sync when entering watching mode so stream
requests are picked up without waiting for the next scheduled interval
- Consolidate 3 maps (active, taskDirs, taskTrackers) into single
activeDownload struct — eliminates out-of-sync state on mid-function panic
- Cancel() runs os.RemoveAll in background goroutine (non-blocking)
- Flush(): clear dirty before unlock to prevent concurrent flush race on
same tmp file; remove fragile re-mark-on-error pattern
- Revert RWMutex → Mutex in ProgressTracker (negligible benefit under
write-heavy workload, higher overhead)
- Remove file.Sync() from debrid and usenet downloaders (Close flushes
kernel buffers; fsync blocks for seconds on large files)
- Pin golangci-lint to v2.1.6 in CI (was floating with `latest`)
- Fix CI matrix: Go 1.25+1.26 (was 1.24+1.25, but go.mod requires 1.25)
- Go deps: cobra 1.10.2, fatih/color 1.19, tablewriter 1.1.4,
anacrolix/torrent 1.61, charmbracelet/huh 1.0, pion/webrtc 4.2.11
- GitHub Actions: checkout v6, setup-go v6, golangci-lint-action v9,
codecov-action v5, ghaction-upx v4, goreleaser-action v7
- CI matrix: drop Go 1.22, test on 1.24 + 1.25
- Migrate tablewriter API from v0 to v1 (breaking change)
- Fix data race in WSTransport.readLoop (pass conn as parameter)
- Add file.Sync() before close in debrid and usenet downloaders
- Improve progress tracker: dedup MarkDone, re-mark dirty on flush error
- Add daemon state persistence and stale resume file cleanup
- Add TriggerPoll for WebSocket resume actions
- Improve stream server with graceful shutdown and connection tracking
- Add desktop notifications for download completion
- Add media file organization with Movies/TV Shows detection
- Improve usenet downloader with progress tracking and resume support
- Add self-update package with GitHub release verification
- Downgrade tablewriter to v0.0.5 (v1.x API breaking change)
Complete usenet download support for unarr CLI:
- NZB XML parser with password extraction from <head> meta
- yEnc decoder with CRC32 verification
- NNTP client with TLS, auth, and connection pool (up to 10 conns)
- Segment downloader with parallel workers and progress reporting
- Post-processing: par2 verify/repair, unrar/7z extraction with password support
- Agent client methods: SearchNzbs, DownloadNzb, GetUsenetCredentials
- UsenetDownloader implementing full Downloader interface
- Daemon wiring: UsenetDownloader passed to Manager
E2E tested: Oppenheimer 1080p (2.94 GB) downloaded via NNTP in 77.6s.