package httpdl import ( "encoding/json" "os" "path/filepath" "sync/atomic" ) // control is the on-disk resume state, a small JSON sidecar next to the output // file (.got). It is a Go-simple binary-free control file: enough to skip // finished segments and resume partial ones, plus validators (Total + // ETag/LastModified) so we never trust a stale file. type control struct { URL string `json:"url"` Total int64 `json:"total"` ETag string `json:"etag,omitempty"` LastModified string `json:"last_modified,omitempty"` Segs []segState `json:"segs"` } type segState struct { Start int64 `json:"start"` End int64 `json:"end"` Written int64 `json:"written"` } func controlPath(out string) string { return out + ".got" } // snapshot builds a control record from the live segments. The segment slice is // fixed once the file is divided and each segment's frontier is owned by one // worker, so reading written atomically gives a consistent record with no lock. func snapshot(url string, total int64, etag, lastmod string, segs []seg) control { c := control{URL: url, Total: total, ETag: etag, LastModified: lastmod, Segs: make([]segState, len(segs))} for i := range segs { c.Segs[i] = segState{segs[i].start, segs[i].end, atomic.LoadInt64(&segs[i].written)} } return c } // save writes the control file atomically (temp + rename). func (c control) save(out string) error { data, err := json.Marshal(c) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil { return err } tmp := controlPath(out) + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, controlPath(out)) } // loadControl reads a control file if it is present and still matches the // download (same URL, length and validator). It returns nil when there is // nothing trustworthy to resume from. func loadControl(out, url string, total int64, etag, lastmod string) *control { data, err := os.ReadFile(controlPath(out)) if err != nil { return nil } var c control if json.Unmarshal(data, &c) != nil { return nil } if c.URL != url || c.Total != total { return nil } if (etag != "" || c.ETag != "") && c.ETag != etag { return nil } if (lastmod != "" || c.LastModified != "") && c.LastModified != lastmod { return nil } // Reject a control file whose segments do not exactly tile [0,total): a // truncated/corrupted/hand-edited sidecar that still parses as JSON could // otherwise mark a segment done() without its bytes on disk (inflated Written) // or leave an un-downloaded hole, both of which would be reported as a complete // file. We restart cleanly instead of trusting it. if !validSegs(c.Segs, c.Total) { return nil } return &c } // validSegs reports whether segs cover [0,total) with no gap or overlap and a // sane written count for each. got always writes segments in ascending start // order, so coverage is checked in place; an out-of-order sidecar (only possible // from a hand-edited or corrupted file) is rejected, which restarts cleanly. func validSegs(segs []segState, total int64) bool { if total <= 0 || len(segs) == 0 { return false } var next int64 for _, s := range segs { length := s.End - s.Start + 1 if s.Start != next || s.End < s.Start || s.Written < 0 || s.Written > length { return false } next = s.End + 1 } return next == total } func removeControl(out string) { os.Remove(controlPath(out)) }