//go:build linux || freebsd // +build linux freebsd package output import ( "encoding/json" "fmt" "os" "time" "codeberg.org/petrbalvin/goget/internal/core" ) // ResumeMetaFileSuffix is the file suffix used for resume metadata files. const ResumeMetaFileSuffix = ".goget.meta" // ResumeMetadata holds metadata for resuming interrupted downloads. type ResumeMetadata struct { URL string `json:"url"` ETag string `json:"etag,omitempty"` LastModified string `json:"last_modified,omitempty"` Downloaded int64 `json:"downloaded"` Total int64 `json:"total,omitempty"` LastWrite time.Time `json:"last_write"` Version string `json:"version"` Chunks map[int]int64 `json:"chunks,omitempty"` // per-chunk progress for parallel downloads } // NewResumeMetadata creates a new ResumeMetadata instance with the given parameters. func NewResumeMetadata(url, etag, lastModified string, downloaded, total int64) *ResumeMetadata { return &ResumeMetadata{ URL: url, ETag: etag, LastModified: lastModified, Downloaded: downloaded, Total: total, LastWrite: time.Now(), Version: "1.0", } } // Save writes the metadata to a JSON file alongside the downloaded file. func (rm *ResumeMetadata) Save(outputPath string) error { metaPath := outputPath + ResumeMetaFileSuffix data, err := json.MarshalIndent(rm, "", " ") if err != nil { return fmt.Errorf("failed to marshal meta: %w", err) } return os.WriteFile(metaPath, data, 0600) } // LoadResumeMetadata reads resume metadata from the associated JSON file. func LoadResumeMetadata(outputPath string) (*ResumeMetadata, error) { metaPath := outputPath + ResumeMetaFileSuffix data, err := os.ReadFile(metaPath) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("failed to read meta: %w", err) } var rm ResumeMetadata if err := json.Unmarshal(data, &rm); err != nil { return nil, fmt.Errorf("failed to parse metadata: %w", err) } return &rm, nil } // DeleteResumeMetadata removes the metadata file associated with the given output path. func DeleteResumeMetadata(outputPath string) error { metaPath := outputPath + ResumeMetaFileSuffix if err := os.Remove(metaPath); err != nil && !os.IsNotExist(err) { return err } return nil } // CanResume checks whether a download can be resumed by validating the metadata file. // It returns true only if valid metadata exists and matches the requested URL. func CanResume(outputPath string, reqURL string) (bool, *ResumeMetadata, error) { // Check if output file exists info, err := os.Stat(outputPath) if err != nil { if os.IsNotExist(err) { return false, nil, nil } return false, nil, err } // Load metadata meta, err := LoadResumeMetadata(outputPath) if err != nil { return false, nil, err } // Without valid metadata, resume is not safe (file may be from another download or corrupted) if meta == nil { return false, nil, nil } // Verify URL matches if meta.URL != reqURL { return false, nil, fmt.Errorf("url mismatch: metadata for %s, requested %s", meta.URL, reqURL) } // Verify file size matches if info.Size() != meta.Downloaded { return false, nil, fmt.Errorf("size mismatch: file has %d bytes, metadata expects %d", info.Size(), meta.Downloaded) } return true, meta, nil } // GetResumeInfo creates a ResumeInfo from the metadata for use in download resumption. func GetResumeInfo(meta *ResumeMetadata, outputPath string) (*core.ResumeInfo, error) { info, err := os.Stat(outputPath) if err != nil { return nil, err } return &core.ResumeInfo{ DownloadedBytes: meta.Downloaded, ETag: meta.ETag, LastModified: meta.LastModified, URL: meta.URL, LastWrite: info.ModTime(), }, nil } // FileExists checks whether a file exists at the given path. func FileExists(path string) bool { _, err := os.Stat(path) return err == nil } // GetFileSize returns the size of the file at the given path. func GetFileSize(path string) (int64, error) { info, err := os.Stat(path) if err != nil { return 0, err } return info.Size(), nil } // CleanupResumeFiles removes the downloaded file and its associated metadata file. func CleanupResumeFiles(outputPath string) error { if err := os.Remove(outputPath); err != nil && !os.IsNotExist(err) { return err } return DeleteResumeMetadata(outputPath) }