feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//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)
|
||||
}
|
||||
Reference in New Issue
Block a user