//go:build linux || freebsd // +build linux freebsd package api import ( "context" "fmt" "net/url" "time" "codeberg.org/petrbalvin/goget/internal/config" "codeberg.org/petrbalvin/goget/internal/core" "codeberg.org/petrbalvin/goget/internal/metalink" "codeberg.org/petrbalvin/goget/internal/protocol" "codeberg.org/petrbalvin/goget/internal/queue" ) // Version returns the current goget version. func Version() string { return core.Version } // Client is the public API client for goget downloads and uploads. type Client struct { ctx context.Context cancel context.CancelFunc cfg *clientConfig } // ClientOption configures a Client. type ClientOption func(*clientConfig) type clientConfig struct { timeout time.Duration userAgent string verbose bool } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) ClientOption { return func(c *clientConfig) { c.timeout = d } } // WithUserAgent sets the User-Agent header. func WithUserAgent(ua string) ClientOption { return func(c *clientConfig) { c.userAgent = ua } } // WithVerbose enables verbose output. func WithVerbose(v bool) ClientOption { return func(c *clientConfig) { c.verbose = v } } // NewClient creates a new goget client with the given options. // The client registers HTTP and FTP protocol handlers automatically. func NewClient(opts ...ClientOption) *Client { cfg := &clientConfig{ timeout: 30 * time.Minute, userAgent: "Goget/" + Version() + " (Library)", verbose: false, } for _, opt := range opts { opt(cfg) } registerDefaultProtocols() // The client-scoped context is cancellable but has no deadline. It // lives until Close() is called and is the parent of every per-request // context created by requestContext(). A deadline here would cap the // *lifetime* of the client, so a long-lived Client could only serve // requests for cfg.timeout before every subsequent call failed with // "context deadline exceeded". The per-request deadline is applied // below in Download and Upload. ctx, cancel := context.WithCancel(context.Background()) return &Client{ctx: ctx, cancel: cancel, cfg: cfg} } // requestContext returns a child context of the client scope with the // configured per-request timeout applied. If the timeout is zero or // negative, the request inherits the client scope without a deadline. func (c *Client) requestContext() (context.Context, context.CancelFunc) { if c.cfg.timeout > 0 { return context.WithTimeout(c.ctx, c.cfg.timeout) } return context.WithCancel(c.ctx) } // DownloadRequest represents a download request. type DownloadRequest struct { URL string Output string Resume bool Verbose bool } // DownloadResult contains the result of a download. type DownloadResult struct { BytesDownloaded int64 TotalSize int64 Speed float64 Duration time.Duration OutputPath string Protocol string IPVersion int Resumed bool Parallel bool ChunksCount int } // Download downloads a file from the given URL. func (c *Client) Download(req *DownloadRequest) (*DownloadResult, error) { if req == nil || req.URL == "" { return nil, fmt.Errorf("url is required") } parsedURL, err := url.Parse(req.URL) if err != nil { return nil, fmt.Errorf("invalid url: %w", err) } proto, err := protocol.Get(parsedURL) if err != nil { return nil, fmt.Errorf("unsupported protocol: %w", err) } // Apply client defaults if request doesn't override verbose := req.Verbose || c.cfg.verbose headers := make(map[string]string) if c.cfg.userAgent != "" { headers["User-Agent"] = c.cfg.userAgent } coreReq := &core.DownloadRequest{ URL: parsedURL, Output: req.Output, Resume: req.Resume, Verbose: verbose, Headers: headers, Timeout: c.cfg.timeout, } // Derive a per-request context from the client scope so a long-lived // Client can serve many requests and each one gets its own deadline. reqCtx, reqCancel := c.requestContext() defer reqCancel() coreReq.Ctx = reqCtx result, err := proto.Download(reqCtx, coreReq) if err != nil { return nil, err } return &DownloadResult{ BytesDownloaded: result.BytesDownloaded, TotalSize: result.TotalSize, Speed: result.Speed, Duration: result.Duration, OutputPath: result.OutputPath, Protocol: result.Protocol, IPVersion: result.IPVersion, Resumed: result.Resumed, Parallel: result.Parallel, ChunksCount: result.ChunksCount, }, nil } // UploadRequest represents an upload request. type UploadRequest struct { URL string FilePath string Method string // PUT or POST Verbose bool } // UploadResult contains the result of an upload. type UploadResult struct { BytesUploaded int64 Duration time.Duration Protocol string ResultURL string } // Upload uploads a file to the given URL. func (c *Client) Upload(req *UploadRequest) (*UploadResult, error) { if req == nil || req.URL == "" || req.FilePath == "" { return nil, fmt.Errorf("url and file path are required") } parsedURL, err := url.Parse(req.URL) if err != nil { return nil, fmt.Errorf("invalid url: %w", err) } proto, err := protocol.Get(parsedURL) if err != nil { return nil, fmt.Errorf("unsupported protocol: %w", err) } uploader, ok := proto.(core.Uploader) if !ok { return nil, fmt.Errorf("protocol does not support upload: %s", parsedURL.Scheme) } coreReq := &core.UploadRequest{ URL: parsedURL, Input: req.FilePath, Verbose: req.Verbose, } // Per-request context so a long-lived Client can serve many uploads // without each one being bounded by the time elapsed since client // creation. See NewClient for the rationale. reqCtx, reqCancel := c.requestContext() defer reqCancel() coreResult, err := uploader.Upload(reqCtx, coreReq) if err != nil { return nil, err } return &UploadResult{ BytesUploaded: coreResult.BytesUploaded, Duration: coreResult.Duration, Protocol: coreResult.Protocol, ResultURL: coreResult.ResultURL, }, nil } // Close releases resources associated with the client. func (c *Client) Close() { c.cancel() } // Download is a convenience function for a one-shot download. func Download(urlStr, output string) (*DownloadResult, error) { client := NewClient() defer client.Close() return client.Download(&DownloadRequest{ URL: urlStr, Output: output, Resume: true, }) } // Upload is a convenience function for a one-shot upload. func Upload(urlStr, filePath, method string) (*UploadResult, error) { client := NewClient() defer client.Close() return client.Upload(&UploadRequest{ URL: urlStr, FilePath: filePath, Method: method, }) } // ============================================================================ // Queue API // ============================================================================ // QueueClient wraps the download queue for programmatic use. type QueueClient struct { q *queue.Queue } // NewQueueClient creates a new queue client. func NewQueueClient(queueFile string) (*QueueClient, error) { q, err := queue.NewQueue(&queue.QueueConfig{QueueFile: queueFile}) if err != nil { return nil, err } return &QueueClient{q: q}, nil } // Add adds a URL to the download queue. func (qc *QueueClient) Add(urlStr, output string, priority int) string { item := qc.q.Add(urlStr, output, priority) return item.ID } // List returns all items in the queue. func (qc *QueueClient) List() []*queue.QueueItem { return qc.q.GetAll() } // Stats returns queue statistics. func (qc *QueueClient) Stats() queue.QueueStats { return qc.q.GetStats() } // Remove removes an item from the queue by ID. func (qc *QueueClient) Remove(id string) bool { return qc.q.Remove(id) } // ClearCompleted removes all completed items. func (qc *QueueClient) ClearCompleted() int { return qc.q.ClearCompleted() } // ============================================================================ // Metalink API // ============================================================================ // ParseMetalink parses a Metalink file. func ParseMetalink(path string) (*metalink.Metalink, error) { return metalink.ParseFile(path) } // FetchMetalink downloads and parses a Metalink from a URL. func FetchMetalink(urlStr string) (*metalink.Metalink, error) { return metalink.ParseURL(urlStr) } // ============================================================================ // Config API // ============================================================================ // LoadConfig loads the configuration file. func LoadConfig(path string) (*config.Config, error) { return config.Load(path) } // DefaultConfig returns the default configuration. func DefaultConfig() *config.Config { return config.DefaultConfig() } // ParseSpeed parses a speed string like "10MB/s" to bytes/sec. func ParseSpeed(s string) int64 { return config.ParseSpeed(s) }