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

+62 -9
View File
@@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
@@ -18,6 +19,7 @@ import (
"ddc/internal/fetch"
"ddc/internal/files"
"ddc/internal/guard"
"ddc/internal/history"
"ddc/internal/libraries"
"ddc/internal/preferences"
)
@@ -31,6 +33,7 @@ var (
type Manager struct {
settings config.Settings
preferences *preferences.Store
history *history.Store
policy *guard.Policy
downloader *fetch.Downloader
rootContext context.Context
@@ -40,12 +43,13 @@ type Manager struct {
jobs map[string]*Job
}
func NewManager(settings config.Settings, store *preferences.Store) *Manager {
func NewManager(settings config.Settings, store *preferences.Store, historyStore *history.Store) *Manager {
policy := guard.NewPolicy(store.Get().AllowedPorts)
rootContext, cancelAll := context.WithCancel(context.Background())
return &Manager{
settings: settings,
preferences: store,
history: historyStore,
policy: policy,
downloader: fetch.NewDownloader(policy),
rootContext: rootContext,
@@ -63,6 +67,11 @@ func (manager *Manager) Preferences() preferences.Preferences {
}
func (manager *Manager) UpdatePreferences(next preferences.Preferences) error {
downloadPaths, err := manager.normalizeDownloadPaths(next.DownloadPaths)
if err != nil {
return err
}
next.DownloadPaths = downloadPaths
if err := manager.preferences.Update(next); err != nil {
return err
}
@@ -82,11 +91,27 @@ func (manager *Manager) AvailableBytes() (int64, error) {
return diskspace.Available(manager.settings.DownloadDir)
}
func (manager *Manager) Create(rawURL string, autoExtract bool) (Snapshot, error) {
target, err := manager.policy.ValidateURL(rawURL)
type CreateRequest struct {
URL string `json:"url"`
FileName string `json:"fileName"`
AutoExtract bool `json:"autoExtract"`
Destination string `json:"destination"`
Subfolder string `json:"subfolder"`
}
func (manager *Manager) Create(request CreateRequest) (Snapshot, error) {
target, err := manager.policy.ValidateURL(request.URL)
if err != nil {
return Snapshot{}, invalid(err)
}
requestedName := fetch.SanitizeFileName(request.FileName)
if strings.TrimSpace(request.FileName) != "" && requestedName == "" {
return Snapshot{}, invalid(errors.New("file name is not allowed"))
}
moveTarget, err := manager.resolveTarget(request.Destination, request.Subfolder)
if err != nil {
return Snapshot{}, err
}
if err := diskspace.EnsureFree(manager.settings.DownloadDir, 0, manager.reservedBytes()); err != nil {
return Snapshot{}, err
}
@@ -103,13 +128,16 @@ func (manager *Manager) Create(rawURL string, autoExtract bool) (Snapshot, error
job := manager.newJob(id, directory, time.Now())
job.sourceURL = target.String()
job.displayURL = target.Redacted()
job.autoExtract = autoExtract
job.requestedName = requestedName
job.autoExtract = request.AutoExtract
job.target = moveTarget
job.status = StatusQueued
job.fileName = initialFileName(target.Path, target.Host)
job.fileName = initialFileName(requestedName, target.Path, target.Host)
job.workers.Add(1)
manager.store(job)
go manager.download(job)
job.saveRecord()
manager.launchDownload(job)
return job.snapshot(), nil
}
@@ -117,7 +145,11 @@ func (manager *Manager) List() []Snapshot {
manager.mutex.RLock()
snapshots := make([]Snapshot, 0, len(manager.jobs))
for _, job := range manager.jobs {
snapshots = append(snapshots, job.snapshot())
snapshot := job.snapshot()
if snapshot.Status == StatusQueued {
snapshot.QueuePosition = manager.slots.position(job.id)
}
snapshots = append(snapshots, snapshot)
}
manager.mutex.RUnlock()
@@ -157,8 +189,26 @@ func (manager *Manager) Delete(id string) error {
return os.RemoveAll(job.directory)
}
func (manager *Manager) CancelAll() {
func (manager *Manager) Shutdown(ctx context.Context) {
manager.cancelAll()
manager.mutex.RLock()
allJobs := make([]*Job, 0, len(manager.jobs))
for _, job := range manager.jobs {
allJobs = append(allJobs, job)
}
manager.mutex.RUnlock()
finished := make(chan struct{})
go func() {
for _, job := range allJobs {
job.workers.Wait()
}
close(finished)
}()
select {
case <-finished:
case <-ctx.Done():
}
}
func (manager *Manager) newJob(id, directory string, createdAt time.Time) *Job {
@@ -196,7 +246,10 @@ func newJobID() (string, error) {
return hex.EncodeToString(buffer), nil
}
func initialFileName(urlPath, host string) string {
func initialFileName(requestedName, urlPath, host string) string {
if requestedName != "" {
return requestedName
}
if name := fetch.SanitizeFileName(path.Base(urlPath)); name != "" {
return name
}