fix(engine): cross-backend integrity guard with retry-then-damaged

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.
This commit is contained in:
Deivid Soto 2026-06-17 12:51:47 +02:00
parent 271413e0f9
commit a5f3f0914a
13 changed files with 400 additions and 91 deletions

View file

@ -277,10 +277,16 @@ func (u *UsenetDownloader) Download(ctx context.Context, task *Task, outputDir s
log.Printf("[%s] extracted archive", shortID)
}
if ppResult.VerifyNote != "" {
// Degraded verification (par2 missing / repair failed): surface it loudly
// so the delivered file isn't silently assumed good.
// Degraded verification (par2 missing / transient probe error): surface it
// loudly so the delivered file isn't silently assumed good.
log.Printf("[%s] WARNING: %s", shortID, ppResult.VerifyNote)
}
if ppResult.Corrupt {
// par2 DEFINITIVELY confirmed unrepairable damage — fail as an integrity
// error so the manager re-downloads clean instead of completing a corrupt
// release (symmetric with the debrid/torrent guards).
return nil, integrityErr("par2_failed", "usenet delivery is corrupt: %s", ppResult.VerifyNote)
}
finalPath := ppResult.FinalPath
if finalPath == "" {