342 lines
8.8 KiB
Go
342 lines
8.8 KiB
Go
//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)
|
|
}
|