feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TimeoutConfig configures smart timeout
|
||||
type TimeoutConfig struct {
|
||||
Min time.Duration
|
||||
Max time.Duration
|
||||
MinSpeedBytesPerSec int64
|
||||
SafetyFactor float64
|
||||
DefaultUnknown time.Duration
|
||||
}
|
||||
|
||||
// DefaultTimeoutConfig returns default settings
|
||||
func DefaultTimeoutConfig() *TimeoutConfig {
|
||||
return &TimeoutConfig{
|
||||
Min: 30 * time.Second,
|
||||
Max: 24 * time.Hour,
|
||||
MinSpeedBytesPerSec: 10 * 1024,
|
||||
SafetyFactor: 3.0,
|
||||
DefaultUnknown: 30 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTimeout calculates the recommended timeout
|
||||
func (tc *TimeoutConfig) CalculateTimeout(fileSize int64, userOverride time.Duration) time.Duration {
|
||||
if userOverride > 0 {
|
||||
return userOverride
|
||||
}
|
||||
if fileSize <= 0 {
|
||||
return tc.DefaultUnknown
|
||||
}
|
||||
expectedSeconds := float64(fileSize) / float64(tc.MinSpeedBytesPerSec)
|
||||
calculated := time.Duration(expectedSeconds * tc.SafetyFactor * float64(time.Second))
|
||||
if calculated < tc.Min {
|
||||
return tc.Min
|
||||
}
|
||||
if calculated > tc.Max {
|
||||
return tc.Max
|
||||
}
|
||||
return calculated
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RequestContext holds metadata about the request across layers
|
||||
type RequestContext struct {
|
||||
// Request ID for logging
|
||||
RequestID string
|
||||
|
||||
// Start of operation
|
||||
StartTime time.Time
|
||||
|
||||
// Protocols used in the chain
|
||||
ProtocolChain []string
|
||||
|
||||
// IP address to which was connected
|
||||
ConnectedIP string
|
||||
|
||||
// IP version (4 or 6)
|
||||
IPVersion int
|
||||
|
||||
// Number of attempts
|
||||
AttemptCount int
|
||||
|
||||
// Context for cancellation
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
// NewRequestContext creates a new request context
|
||||
func NewRequestContext(ctx context.Context, requestID string) *RequestContext {
|
||||
return &RequestContext{
|
||||
RequestID: requestID,
|
||||
StartTime: time.Now(),
|
||||
ProtocolChain: make([]string, 0),
|
||||
IPVersion: 0,
|
||||
AttemptCount: 0,
|
||||
Context: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Duration returns the duration of operation
|
||||
func (rc *RequestContext) Duration() time.Duration {
|
||||
return time.Since(rc.StartTime)
|
||||
}
|
||||
|
||||
// AddProtocol adds a protocol to the chain
|
||||
func (rc *RequestContext) AddProtocol(protocol string) {
|
||||
rc.ProtocolChain = append(rc.ProtocolChain, protocol)
|
||||
}
|
||||
|
||||
// IsCancelled returns whether the context was cancelled
|
||||
func (rc *RequestContext) IsCancelled() bool {
|
||||
select {
|
||||
case <-rc.Context.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ErrorType defines the error type
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
ErrNetwork ErrorType = "NETWORK"
|
||||
ErrProtocol ErrorType = "PROTOCOL"
|
||||
ErrFile ErrorType = "FILE"
|
||||
ErrTimeout ErrorType = "TIMEOUT"
|
||||
ErrAuth ErrorType = "AUTH"
|
||||
ErrChecksum ErrorType = "CHECKSUM"
|
||||
ErrUnsupported ErrorType = "UNSUPPORTED"
|
||||
ErrCancelled ErrorType = "CANCELLED"
|
||||
)
|
||||
|
||||
// GogetError is a structured error with context
|
||||
type GogetError struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
Cause error
|
||||
URL string
|
||||
Hint string // Actionable suggestion for the user
|
||||
Details map[string]string
|
||||
}
|
||||
|
||||
func (e *GogetError) Error() string {
|
||||
var msg string
|
||||
if e.Cause != nil {
|
||||
msg = fmt.Sprintf("[%s] %s: %v", e.Type, e.Message, e.Cause)
|
||||
} else {
|
||||
msg = fmt.Sprintf("[%s] %s", e.Type, e.Message)
|
||||
}
|
||||
if e.Hint != "" {
|
||||
msg += fmt.Sprintf(" (hint: %s)", e.Hint)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *GogetError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// GetDetail returns a value from the Details map, or an empty string if the key is absent.
|
||||
func (e *GogetError) GetDetail(key string) string {
|
||||
if e.Details == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Details[key]
|
||||
}
|
||||
|
||||
// NewNetworkError creates a network error
|
||||
func NewNetworkError(msg string, cause error, url string) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrNetwork,
|
||||
Message: msg,
|
||||
Cause: cause,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewProtocolError creates a protocol error
|
||||
func NewProtocolError(msg string, cause error, url string) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrProtocol,
|
||||
Message: msg,
|
||||
Cause: cause,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileError creates a file error
|
||||
func NewFileError(msg string, cause error) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrFile,
|
||||
Message: msg,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTimeoutError creates a timeout error
|
||||
func NewTimeoutError(msg string, url string) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrTimeout,
|
||||
Message: msg,
|
||||
URL: url,
|
||||
Hint: "increase --max-time or --connect-timeout, or check network connectivity",
|
||||
}
|
||||
}
|
||||
|
||||
// NewChecksumError creates a checksum error
|
||||
func NewChecksumError(expected, actual string) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrChecksum,
|
||||
Message: fmt.Sprintf("checksum mismatch: expected %s, got %s", expected, actual),
|
||||
Hint: "verify the file integrity with an external checksum tool or re-download",
|
||||
Details: map[string]string{
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnsupportedError creates an unsupported operation error
|
||||
func NewUnsupportedError(msg string) *GogetError {
|
||||
return &GogetError{
|
||||
Type: ErrUnsupported,
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// WithHint returns the error with the hint set. Use for adding contextual,
|
||||
// actionable suggestions to an existing error without creating a new one.
|
||||
func (e *GogetError) WithHint(hint string) *GogetError {
|
||||
e.Hint = hint
|
||||
return e
|
||||
}
|
||||
|
||||
// SafeURL returns the URL string with credentials removed for safe error logging.
|
||||
func SafeURL(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
clean := *u
|
||||
clean.User = nil
|
||||
return clean.String()
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParallelConfig configures parallel downloading
|
||||
type ParallelConfig struct {
|
||||
Connections int
|
||||
MinSize int64
|
||||
MinChunkSize int64
|
||||
MaxChunkSize int64
|
||||
}
|
||||
|
||||
// DefaultParallelConfig returns default settings
|
||||
func DefaultParallelConfig() *ParallelConfig {
|
||||
return &ParallelConfig{
|
||||
Connections: 0,
|
||||
MinSize: 100 * 1024 * 1024,
|
||||
MinChunkSize: 1 * 1024 * 1024,
|
||||
MaxChunkSize: 50 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
|
||||
// GetConnections returns the number of connections
|
||||
func (pc *ParallelConfig) GetConnections(fileSize int64) int {
|
||||
if pc.Connections > 0 {
|
||||
return pc.Connections
|
||||
}
|
||||
if fileSize >= pc.MinSize {
|
||||
return 4
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// ChunkInfo represents one file segment
|
||||
type ChunkInfo struct {
|
||||
ID int
|
||||
Start int64
|
||||
End int64
|
||||
Downloaded int64
|
||||
Status int
|
||||
Error error
|
||||
}
|
||||
|
||||
// ResumeInfo contains metadata for continuation
|
||||
type ResumeInfo struct {
|
||||
DownloadedBytes int64
|
||||
ETag string
|
||||
LastModified string
|
||||
URL string
|
||||
LastWrite time.Time
|
||||
Chunks map[int]int64
|
||||
}
|
||||
|
||||
// DownloadRequest represents a download request
|
||||
type DownloadRequest struct {
|
||||
URL *url.URL
|
||||
Output string
|
||||
Writer io.Writer
|
||||
Resume bool
|
||||
Timeout time.Duration
|
||||
Verbose bool
|
||||
DebugTransport bool
|
||||
Checksum string
|
||||
Headers map[string]string
|
||||
Proxy string
|
||||
AutoDecompress bool
|
||||
ProgressCallback func(current, total int64, speed float64)
|
||||
ResumeInfo *ResumeInfo
|
||||
Parallel *ParallelConfig
|
||||
Recursive bool
|
||||
RecursiveParallel int // worker pool size for recursive downloads (0 = sequential)
|
||||
DryRun bool // list what would be downloaded, do not write any files
|
||||
MaxDepth int
|
||||
MaxFileSize int64 // max file size to download (0 = unlimited)
|
||||
AcceptPatterns []string // glob patterns for accepted files (e.g., "*.pdf,*.html")
|
||||
RejectPatterns []string // glob patterns for rejected files (e.g., "*.tmp,*.bak")
|
||||
KeepTimestamps bool // set file modification time from server response
|
||||
Ctx context.Context
|
||||
SSHKnownHosts string // path to known_hosts file (SFTP)
|
||||
SSHInsecure bool // skip SSH host key verification (SFTP)
|
||||
}
|
||||
|
||||
// DownloadResult represents a download result
|
||||
type DownloadResult struct {
|
||||
BytesDownloaded int64
|
||||
TotalSize int64
|
||||
Duration time.Duration
|
||||
Protocol string
|
||||
IPVersion int
|
||||
Speed float64
|
||||
OutputPath string
|
||||
Checksum string
|
||||
Resumed bool
|
||||
Parallel bool
|
||||
ChunksCount int
|
||||
HTTPStatusCode int // HTTP status code from response
|
||||
TraceTimings *TraceTimings // HTTP tracing breakdown
|
||||
}
|
||||
|
||||
// TraceTimings holds HTTP request timing breakdown.
|
||||
type TraceTimings struct {
|
||||
DNSLookup time.Duration
|
||||
TCPConnect time.Duration
|
||||
TLSHandshake time.Duration
|
||||
TTFB time.Duration // Time To First Byte
|
||||
Total time.Duration
|
||||
}
|
||||
|
||||
func (tt *TraceTimings) String() string {
|
||||
return fmt.Sprintf(
|
||||
"DNS: %v | TCP: %v | TLS: %v | TTFB: %v | Total: %v",
|
||||
tt.DNSLookup.Round(time.Millisecond),
|
||||
tt.TCPConnect.Round(time.Millisecond),
|
||||
tt.TLSHandshake.Round(time.Millisecond),
|
||||
tt.TTFB.Round(time.Millisecond),
|
||||
tt.Total.Round(time.Millisecond),
|
||||
)
|
||||
}
|
||||
|
||||
// Protocol is the interface for protocols
|
||||
type Protocol interface {
|
||||
Scheme() string
|
||||
CanHandle(url *url.URL) bool
|
||||
Download(ctx context.Context, req *DownloadRequest) (*DownloadResult, error)
|
||||
Capabilities() []string
|
||||
SupportsResume() bool
|
||||
SupportsCompression() bool
|
||||
SupportsParallel() bool
|
||||
SupportsRecursive() bool
|
||||
}
|
||||
|
||||
// UploadRequest represents an upload request
|
||||
type UploadRequest struct {
|
||||
URL *url.URL
|
||||
Input string
|
||||
Reader io.Reader
|
||||
Timeout time.Duration
|
||||
Verbose bool
|
||||
Headers map[string]string
|
||||
Proxy string
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// UploadResult represents an upload result
|
||||
type UploadResult struct {
|
||||
BytesUploaded int64
|
||||
Duration time.Duration
|
||||
Protocol string
|
||||
ResultURL string
|
||||
}
|
||||
|
||||
// Uploader is the interface for protocols with upload support
|
||||
type Uploader interface {
|
||||
Upload(ctx context.Context, req *UploadRequest) (*UploadResult, error)
|
||||
SupportsUpload() bool
|
||||
}
|
||||
|
||||
// ProgressCallback is a function for progress reporting
|
||||
type ProgressCallback func(current, total int64, speed float64)
|
||||
|
||||
// ProgressWriter is a writer with progress support
|
||||
type ProgressWriter interface {
|
||||
io.Writer
|
||||
SetProgressCallback(cb ProgressCallback)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build linux || freebsd
|
||||
|
||||
// Package version exposes the goget release identity.
|
||||
package core
|
||||
|
||||
// Name is the program name reported by `--version` and in log lines.
|
||||
const Name = "goget"
|
||||
|
||||
// Version is the goget release version. Follows SemVer.
|
||||
var Version = "1.0.0"
|
||||
Reference in New Issue
Block a user