feat(downloads): pre-flight free-disk guard before each download (hueco medio)

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.
This commit is contained in:
Deivid Soto 2026-05-31 21:48:34 +02:00
parent 2be92516c6
commit 1cad73b9a7
9 changed files with 196 additions and 4 deletions

View file

@ -293,6 +293,15 @@ func runDaemonStart() error {
// Create debrid downloader
debridDl := engine.NewDebridDownloader()
usenetDl := engine.NewUsenetDownloader(agentClient)
// Pre-flight disk reserve: refuse a download that would leave less than this
// many bytes free, so a download never fills the filesystem to 0 mid-write.
minFreeBytes := int64(cfg.Download.MinFreeDiskMB) << 20
torrentDl.SetMinFreeBytes(minFreeBytes)
debridDl.SetMinFreeBytes(minFreeBytes)
usenetDl.SetMinFreeBytes(minFreeBytes)
log.Printf("[disk] download free-space reserve: %d MiB", cfg.Download.MinFreeDiskMB)
// Create download manager
manager := engine.NewManager(engine.ManagerConfig{
@ -305,7 +314,7 @@ func runDaemonStart() error {
TVShowsDir: cfg.Organize.TVShowsDir,
OutputDir: cfg.Download.Dir,
},
}, reporter, torrentDl, debridDl, engine.NewUsenetDownloader(agentClient))
}, reporter, torrentDl, debridDl, usenetDl)
// Create persistent stream server
streamSrv := engine.NewStreamServer(cfg.Download.StreamPort)