155 lines
4.4 KiB
Go
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)
|
|
}
|