feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/auth"
|
||||
format "codeberg.org/petrbalvin/goget/internal/format"
|
||||
)
|
||||
|
||||
// UploadRequest represents an upload request
|
||||
type UploadRequest struct {
|
||||
// URL to upload to
|
||||
URL string
|
||||
|
||||
// Method: PUT or POST
|
||||
Method string
|
||||
|
||||
// Path to the file for upload
|
||||
FilePath string
|
||||
|
||||
// Form data fields (for POST)
|
||||
FormData map[string]string
|
||||
|
||||
// Files for upload (for POST multipart)
|
||||
Files []UploadFile
|
||||
|
||||
// Custom headers
|
||||
Headers map[string]string
|
||||
|
||||
// Timeout
|
||||
Timeout time.Duration
|
||||
|
||||
// Callback pro progress
|
||||
ProgressCallback func(current, total int64, speed float64)
|
||||
}
|
||||
|
||||
// UploadFile represents a file for multipart upload
|
||||
type UploadFile struct {
|
||||
// Field name in the form data
|
||||
FieldName string
|
||||
|
||||
// Path to the file
|
||||
FilePath string
|
||||
|
||||
// Custom file name (optional)
|
||||
FileName string
|
||||
|
||||
// Content-Type (optional)
|
||||
ContentType string
|
||||
}
|
||||
|
||||
// UploadResult is the result of an upload
|
||||
type UploadResult struct {
|
||||
StatusCode int
|
||||
ContentLength int64
|
||||
Duration time.Duration
|
||||
BytesUploaded int64
|
||||
Speed float64
|
||||
ResponseBody string
|
||||
}
|
||||
|
||||
// Upload uploads a file using PUT or POST
|
||||
func (c *Client) Upload(ctx context.Context, req *UploadRequest) (*UploadResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
if req.Method == "" {
|
||||
req.Method = "PUT"
|
||||
}
|
||||
|
||||
// Create request body
|
||||
var body io.Reader
|
||||
var contentType string
|
||||
var totalSize int64
|
||||
|
||||
if req.Method == "PUT" {
|
||||
// PUT upload - direct file
|
||||
if req.FilePath == "" {
|
||||
return nil, fmt.Errorf("put upload requires --upload-file")
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(req.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
totalSize = fileInfo.Size()
|
||||
|
||||
file, err := os.Open(req.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body = file
|
||||
contentType = "application/octet-stream"
|
||||
|
||||
} else if req.Method == "POST" {
|
||||
// POST upload - form data or multipart
|
||||
if len(req.Files) > 0 {
|
||||
// Multipart form data
|
||||
var bodyBuffer bytes.Buffer
|
||||
writer := multipart.NewWriter(&bodyBuffer)
|
||||
|
||||
// Add regular form fields
|
||||
for key, value := range req.FormData {
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
return nil, fmt.Errorf("failed to write form field: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add files
|
||||
for _, uf := range req.Files {
|
||||
file, err := os.Open(uf.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file %s: %w", uf.FilePath, err)
|
||||
}
|
||||
|
||||
fileName := uf.FileName
|
||||
if fileName == "" {
|
||||
fileName = filepath.Base(uf.FilePath)
|
||||
}
|
||||
|
||||
contentType := uf.ContentType
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Disposition",
|
||||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`, uf.FieldName, fileName))
|
||||
h.Set("Content-Type", contentType)
|
||||
|
||||
part, err := writer.CreatePart(h)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("failed to create form part: %w", err)
|
||||
}
|
||||
|
||||
fileInfo, _ := os.Stat(uf.FilePath)
|
||||
totalSize += fileInfo.Size()
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write file to form: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
|
||||
}
|
||||
|
||||
body = &bodyBuffer
|
||||
contentType = writer.FormDataContentType()
|
||||
|
||||
} else if len(req.FormData) > 0 {
|
||||
// URL-encoded form data
|
||||
form := url.Values{}
|
||||
for key, value := range req.FormData {
|
||||
form.Set(key, value)
|
||||
}
|
||||
|
||||
body = strings.NewReader(form.Encode())
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
totalSize = int64(len(form.Encode()))
|
||||
|
||||
} else if req.FilePath != "" {
|
||||
// POST with raw file body
|
||||
fileInfo, err := os.Stat(req.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
totalSize = fileInfo.Size()
|
||||
|
||||
file, err := os.Open(req.FilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body = file
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
return nil, fmt.Errorf("no upload data provided")
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
httpReq.Header.Set("Content-Type", contentType)
|
||||
httpReq.Header.Set("User-Agent", c.config.UserAgent)
|
||||
|
||||
for key, value := range req.Headers {
|
||||
httpReq.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// Apply authentication
|
||||
if c.config.Auth.Username != "" {
|
||||
authType := c.config.Auth.AuthType
|
||||
if authType == "" {
|
||||
authType = "auto"
|
||||
}
|
||||
|
||||
if authType == "auto" || authType == "basic" {
|
||||
// Try basic auth first
|
||||
applyBasicAuth(httpReq, c.config.Auth.Username, c.config.Auth.Password)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply OAuth token if configured
|
||||
if c.config.Auth.OAuth != nil && c.config.Auth.OAuth.AccessToken != "" {
|
||||
oauthClient := auth.NewOAuthClient(c.config.Auth.OAuth)
|
||||
if oauthClient.IsTokenValid() {
|
||||
oauthClient.ApplyToken(httpReq)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute request
|
||||
|
||||
if c.config.RateLimiter != nil {
|
||||
// Wrap body with rate limiter for upload
|
||||
body = c.config.RateLimiter.WrapReader(body)
|
||||
httpReq.Body = io.NopCloser(body)
|
||||
}
|
||||
|
||||
// Track progress
|
||||
if req.ProgressCallback != nil && totalSize > 0 {
|
||||
body = &progressReader{
|
||||
r: body,
|
||||
callback: req.ProgressCallback,
|
||||
total: totalSize,
|
||||
start: startTime,
|
||||
offset: 0,
|
||||
}
|
||||
httpReq.Body = io.NopCloser(body)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
return &UploadResult{
|
||||
StatusCode: resp.StatusCode,
|
||||
ContentLength: resp.ContentLength,
|
||||
Duration: duration,
|
||||
BytesUploaded: totalSize,
|
||||
Speed: float64(totalSize) / duration.Seconds(),
|
||||
ResponseBody: string(respBody),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadSimple performs a simple file upload with progress tracking
|
||||
func (c *Client) UploadSimple(ctx context.Context, method, targetURL, filePath string, verbose bool) (*UploadResult, error) {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
|
||||
totalSize := fileInfo.Size()
|
||||
_ = totalSize // Used in progress callback
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var progressCallback func(int64, int64, float64)
|
||||
if verbose {
|
||||
progressCallback = func(current, total int64, speed float64) {
|
||||
percent := float64(current) / float64(total) * 100
|
||||
fmt.Fprintf(os.Stderr, "\r[upload] %s/%s (%.1f%%) %s/s",
|
||||
format.Bytes(current),
|
||||
format.Bytes(total),
|
||||
percent,
|
||||
format.Speed(speed))
|
||||
}
|
||||
}
|
||||
|
||||
req := &UploadRequest{
|
||||
URL: targetURL,
|
||||
Method: method,
|
||||
FilePath: filePath,
|
||||
Timeout: c.config.Transport.Timeout,
|
||||
ProgressCallback: progressCallback,
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
// Copy headers from client config
|
||||
for k, v := range c.config.Headers {
|
||||
req.Headers[k] = v
|
||||
}
|
||||
|
||||
result, err := c.Upload(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UploadMultipart performs a multipart POST upload with files
|
||||
func (c *Client) UploadMultipart(ctx context.Context, targetURL string, files []UploadFile, formData map[string]string, verbose bool) (*UploadResult, error) {
|
||||
var progressCallback func(int64, int64, float64)
|
||||
if verbose {
|
||||
progressCallback = func(current, total int64, speed float64) {
|
||||
percent := float64(current) / float64(total) * 100
|
||||
fmt.Fprintf(os.Stderr, "\r[upload] %s/%s (%.1f%%) %s/s",
|
||||
format.Bytes(current),
|
||||
format.Bytes(total),
|
||||
percent,
|
||||
format.Speed(speed))
|
||||
}
|
||||
}
|
||||
|
||||
req := &UploadRequest{
|
||||
URL: targetURL,
|
||||
Method: "POST",
|
||||
Files: files,
|
||||
FormData: formData,
|
||||
Timeout: c.config.Transport.Timeout,
|
||||
ProgressCallback: progressCallback,
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
for k, v := range c.config.Headers {
|
||||
req.Headers[k] = v
|
||||
}
|
||||
|
||||
result, err := c.Upload(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// applyBasicAuth applies HTTP Basic Authentication to request
|
||||
func applyBasicAuth(req *http.Request, username, password string) {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
Reference in New Issue
Block a user