package jobs import ( "encoding/json" "log/slog" "os" "path/filepath" "time" "ddc/internal/fetch" "ddc/internal/files" ) const ( phaseDownloading = "downloading" phasePaused = "paused" phaseFailed = "failed" phaseDone = "done" ) type jobRecord struct { SourceURL string `json:"sourceUrl"` DisplayURL string `json:"displayUrl"` RequestedName string `json:"requestedName,omitempty"` FileName string `json:"fileName"` AutoExtract bool `json:"autoExtract"` Destination string `json:"destination,omitempty"` Subfolder string `json:"subfolder,omitempty"` Phase string `json:"phase"` Message string `json:"message,omitempty"` Download *fetch.State `json:"download,omitempty"` CreatedAt time.Time `json:"createdAt"` } func (job *Job) record() jobRecord { job.mutex.Lock() defer job.mutex.Unlock() record := jobRecord{ SourceURL: job.sourceURL, DisplayURL: job.displayURL, RequestedName: job.requestedName, FileName: job.fileName, AutoExtract: job.autoExtract, Phase: phaseFor(job.status, job.downloaded), Message: job.message, Download: job.downloadState, CreatedAt: job.createdAt, } if job.target != nil { record.Destination = job.target.destination record.Subfolder = job.target.subfolder } return record } func phaseFor(status Status, downloaded bool) string { switch { case downloaded: return phaseDone case status == StatusPaused: return phasePaused case status == StatusFailed: return phaseFailed default: return phaseDownloading } } func (job *Job) saveRecord() { data, err := json.MarshalIndent(job.record(), "", " ") if err != nil { job.logger().Error("could not encode download state", "error", err) return } job.recordMutex.Lock() defer job.recordMutex.Unlock() if err := files.WriteAtomically(filepath.Join(job.directory, files.JobFileName), data); err != nil && !os.IsNotExist(err) { job.logger().Error("could not save download state", "error", err) } } func readRecord(directory string) (jobRecord, bool) { recordPath := filepath.Join(directory, files.JobFileName) data, err := os.ReadFile(recordPath) if err != nil { if !os.IsNotExist(err) { slog.Warn("could not read saved download state", "path", recordPath, "error", err) } return jobRecord{}, false } var record jobRecord if err := json.Unmarshal(data, &record); err != nil { slog.Warn("saved download state is damaged, treating the download as finished", "path", recordPath, "error", err) return jobRecord{}, false } return record, true } func (job *Job) applyRecord(record jobRecord) { job.sourceURL = record.SourceURL job.displayURL = record.DisplayURL job.requestedName = record.RequestedName job.fileName = record.FileName job.autoExtract = record.AutoExtract if !record.CreatedAt.IsZero() { job.createdAt = record.CreatedAt } if record.Destination != "" { job.target = &moveTarget{destination: record.Destination, subfolder: record.Subfolder} } }