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:
parent
2be92516c6
commit
1cad73b9a7
9 changed files with 196 additions and 4 deletions
|
|
@ -90,8 +90,15 @@ type TorrentDownloader struct {
|
|||
|
||||
activeMu sync.Mutex
|
||||
active map[string]*torrent.Torrent // taskID -> torrent handle
|
||||
|
||||
minFreeBytes int64 // disk reserve for the pre-flight space check (0 = reserve disabled)
|
||||
}
|
||||
|
||||
// SetMinFreeBytes sets the free-space reserve enforced before a download starts.
|
||||
// Call once at construction; 0 disables the reserve (the size-vs-free check still
|
||||
// runs). See CheckDiskSpace.
|
||||
func (d *TorrentDownloader) SetMinFreeBytes(n int64) { d.minFreeBytes = n }
|
||||
|
||||
// NewTorrentDownloader creates a BitTorrent downloader with a long-lived client.
|
||||
func NewTorrentDownloader(cfg TorrentConfig) (*TorrentDownloader, error) {
|
||||
// MetadataTimeout: 0 = unlimited (wait forever like qBittorrent)
|
||||
|
|
@ -340,6 +347,15 @@ func (d *TorrentDownloader) Download(ctx context.Context, task *Task, outputDir
|
|||
|
||||
log.Printf("[%s] downloading %s (%s)", task.ID[:8], fileName, formatBytes(totalBytes))
|
||||
|
||||
// 2.5 Pre-flight disk-space guard — refuse before writing rather than fill
|
||||
// the disk to 0 mid-download (corrupts the partial file). Torrents land in
|
||||
// DataDir (not the manager's outputDir), so stat DataDir. Conservative: uses
|
||||
// the full selected size without subtracting pieces a resume may already hold.
|
||||
if err := CheckDiskSpace(d.cfg.DataDir, totalBytes, d.minFreeBytes); err != nil {
|
||||
cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Poll progress with stall detection
|
||||
result, err := d.pollDownload(ctx, t, task, totalBytes, fileName, progressCh)
|
||||
if err != nil {
|
||||
|
|
@ -715,12 +731,15 @@ func formatBytes(b int64) string {
|
|||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
// Cap exp at the last unit so an exabyte-scale value (or a corrupt/huge
|
||||
// size) can never index past the slice and panic.
|
||||
units := []string{"KB", "MB", "GB", "TB", "PB", "EB"}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
for n := b / unit; n >= unit && exp < len(units)-1; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", float64(b)/float64(div), []string{"KB", "MB", "GB", "TB"}[exp])
|
||||
return fmt.Sprintf("%.1f %s", float64(b)/float64(div), units[exp])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue