Files
goget/internal/protocol/http/upload.go
T

380 lines
8.8 KiB
Go

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