Files

155 lines
4.4 KiB
Go

//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)
}