Add resumable downloads, download paths, queue controls and history

This commit is contained in:
SirBlob committed 2026-11-01 13:36:29 -05:00
1 parent 434f6441da
commit 6027cfabb9
43 files changed
+2138 -291

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
package fetch
import (
"context"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
"ddc/internal/diskspace"
)
const (
checkpointInterval = 10 * time.Second
spaceCheckInterval = int64(256) << 20
)
type session struct {
downloader *Downloader
source *remoteFile
file *os.File
request Request
state State
segments []*liveSegment
sinceSpaceCheck atomic.Int64
}
func newSession(downloader *Downloader, source *remoteFile, file *os.File, request Request, state State) *session {
segments := make([]*liveSegment, len(state.Segments))
for index, segment := range state.Segments {
segments[index] = newLiveSegment(segment)
}
return &session{
downloader: downloader,
source: source,
file: file,
request: request,
state: state,
segments: segments,
}
}
func (session *session) run(ctx context.Context, probe *http.Response) error {
session.saveCheckpoint()
stopSaving := make(chan struct{})
savingStopped := make(chan struct{})
go session.saveCheckpoints(stopSaving, savingStopped)
err := session.runWorkers(ctx, probe)
close(stopSaving)
<-savingStopped
session.saveCheckpoint()
return err
}
func (session *session) runWorkers(ctx context.Context, probe *http.Response) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
var workers sync.WaitGroup
var firstFailure sync.Once
var failure error
for index, segment := range session.segments {
if segment.complete() {
continue
}
var initial *http.Response
if index == 0 {
initial, probe = probe, nil
}
worker := &segmentWorker{session: session, segment: segment}
workers.Add(1)
go func() {
defer workers.Done()
if err := worker.run(ctx, initial); err != nil {
firstFailure.Do(func() {
failure = err
cancel(err)
})
}
}()
}
closeBody(probe)
workers.Wait()
return failure
}
func (session *session) record(count int64) error {
session.request.Progress.Add(count)
if session.sinceSpaceCheck.Add(count) < spaceCheckInterval {
return nil
}
session.sinceSpaceCheck.Store(0)
return diskspace.EnsureFree(session.request.Directory, 0, session.request.ReservedBytes)
}
func (session *session) snapshot() State {
state := session.state
state.Segments = make([]Segment, len(session.segments))
for index, segment := range session.segments {
state.Segments[index] = segment.snapshot()
}
return state
}
func (session *session) saveCheckpoints(stop <-chan struct{}, stopped chan<- struct{}) {
defer close(stopped)
ticker := time.NewTicker(checkpointInterval)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
session.saveCheckpoint()
}
}
}
func (session *session) saveCheckpoint() {
if session.request.Checkpoint == nil {
return
}
state := session.snapshot()
session.file.Sync()
session.request.Checkpoint(state)
}