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