64 lines
2.4 KiB
Go
64 lines
2.4 KiB
Go
|
|
package engine
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
|
||
|
|
"github.com/torrentclaw/unarr/internal/agent"
|
||
|
|
)
|
||
|
|
|
||
|
|
// InsufficientDiskError is returned by CheckDiskSpace when a download's expected
|
||
|
|
// size (plus a reserve that keeps the filesystem healthy) won't fit in the free
|
||
|
|
// space of its target directory. The manager treats it as terminal — it does NOT
|
||
|
|
// fall back to another source (a different source would fill the same disk) and
|
||
|
|
// surfaces the message to the web as the task's error.
|
||
|
|
type InsufficientDiskError struct {
|
||
|
|
Dir string
|
||
|
|
Need int64 // bytes the download still needs to write
|
||
|
|
Free int64 // bytes currently free on Dir's filesystem
|
||
|
|
Reserve int64 // bytes to keep free after the download
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *InsufficientDiskError) Error() string {
|
||
|
|
return fmt.Sprintf(
|
||
|
|
"insufficient disk space in %s: need %s + %s reserve, only %s free",
|
||
|
|
e.Dir, formatBytes(e.Need), formatBytes(e.Reserve), formatBytes(e.Free),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsInsufficientDisk reports whether err is (or wraps) an InsufficientDiskError.
|
||
|
|
func IsInsufficientDisk(err error) bool {
|
||
|
|
var d *InsufficientDiskError
|
||
|
|
return errors.As(err, &d)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CheckDiskSpace fails fast when dir's filesystem can't hold needBytes while
|
||
|
|
// keeping reserveBytes free. It's the pre-flight guard so a download never fills
|
||
|
|
// the disk to 0 mid-write (which corrupts the partial file and can wedge the OS).
|
||
|
|
//
|
||
|
|
// Best-effort by design: a non-positive needBytes (size unknown) or a failure to
|
||
|
|
// stat the filesystem returns nil rather than block a download on a guard we
|
||
|
|
// can't evaluate — the OS-level ENOSPC stays the backstop.
|
||
|
|
func CheckDiskSpace(dir string, needBytes, reserveBytes int64) error {
|
||
|
|
if needBytes <= 0 {
|
||
|
|
return nil // size unknown — nothing to check against
|
||
|
|
}
|
||
|
|
free, _, err := agent.DiskInfo(dir)
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("[disk] free-space pre-flight skipped for %q: stat error: %v", dir, err)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if free <= 0 {
|
||
|
|
// Distinct from a stat error: DiskInfo succeeded but reports no free
|
||
|
|
// space. Don't block on a value we can't trust (0/negative) — log it so a
|
||
|
|
// genuinely-full disk is visible rather than masked as a generic skip.
|
||
|
|
log.Printf("[disk] free-space pre-flight skipped for %q: DiskInfo reported non-positive free (%d)", dir, free)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if free-needBytes < reserveBytes {
|
||
|
|
return &InsufficientDiskError{Dir: dir, Need: needBytes, Free: free, Reserve: reserveBytes}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|