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