Files
goget/pkg/api/api.go
T

342 lines
8.8 KiB
Go
Raw Permalink Normal View History

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