remote-task-excutor-cli/pkg/client/http_client.go

341 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
type HTTPClient struct {
Client *http.Client
BaseURL string
Headers map[string]string
}
// NewHTTPClient 创建新的HTTP客户端实例
func NewHTTPClient(baseURL string, timeout time.Duration) *HTTPClient {
return &HTTPClient{
Client: &http.Client{
Timeout: timeout,
},
BaseURL: baseURL,
Headers: make(map[string]string),
}
}
// SetHeader 设置请求头
func (c *HTTPClient) SetHeader(key, value string) {
c.Headers[key] = value
}
// Get 发送GET请求支持查询参数
func (c *HTTPClient) Get(path string, queryParams ...map[string]string) ([]byte, error) {
// 构建完整URL
fullURL := c.BaseURL + path
// 处理查询参数
if len(queryParams) > 0 {
// 创建URL对象
u, err := url.Parse(fullURL)
if err != nil {
return nil, fmt.Errorf("解析URL失败: %w", err)
}
// 添加查询参数
q := u.Query()
for key, value := range queryParams[0] {
q.Add(key, value)
}
u.RawQuery = q.Encode()
fullURL = u.String()
}
// 创建请求
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, err
}
// 设置请求头
for k, v := range c.Headers {
req.Header.Set(k, v)
}
// 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
// 尝试读取响应体以获取更多错误信息
return nil, &HTTPError{
StatusCode: resp.StatusCode,
Message: resp.Status,
}
}
return io.ReadAll(resp.Body)
}
// PostJSON 发送JSON格式POST请求
func (c *HTTPClient) PostJSON(path string, data interface{}) ([]byte, error) {
// 拼接URL
fullURL := c.BaseURL + path
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
fmt.Println("request url, body is :", fullURL, string(jsonData))
req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
for k, v := range c.Headers {
req.Header.Set(k, v)
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &HTTPError{
StatusCode: resp.StatusCode,
Message: resp.Status,
}
}
return io.ReadAll(resp.Body)
}
// UploadFiles 上传多个文件并支持额外表单字段
// files 参数现在支持每个字段名对应多个文件路径
func (c *HTTPClient) UploadFiles(
path string,
filesPrefix string,
files map[string][]string, // 字段名 -> 多个文件路径
formFields map[string]string, // 额外表单字段
) ([]byte, error) {
fullURL := c.BaseURL + path
// 创建multipart writer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加文件部分 - 支持每个字段名多个文件
for fieldName, filePaths := range files {
for _, filePath := range filePaths {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开文件 %s 失败: %w", filePath, err)
}
defer file.Close()
relativePath, err := removePathPrefix(filePath, filesPrefix)
if err != nil {
return nil, err
}
part, err := writer.CreateFormFile(fieldName, url.PathEscape(relativePath))
if err != nil {
return nil, fmt.Errorf("为文件 %s 创建表单字段失败: %w", filePath, err)
}
_, err = io.Copy(part, file)
if err != nil {
return nil, fmt.Errorf("复制文件 %s 内容失败: %w", filePath, err)
}
}
}
// 添加额外表单字段
for key, value := range formFields {
err := writer.WriteField(key, value)
if err != nil {
return nil, fmt.Errorf("添加表单字段 %s 失败: %w", key, err)
}
}
// 关闭writer以完成multipart消息
err := writer.Close()
if err != nil {
return nil, fmt.Errorf("关闭multipart writer失败: %w", err)
}
// 创建请求
req, err := http.NewRequest("POST", fullURL, body)
if err != nil {
return nil, err
}
// 设置内容类型必须包含boundary
contentType := writer.FormDataContentType()
req.Header.Set("Content-Type", contentType)
// 添加自定义头
for k, v := range c.Headers {
req.Header.Set(k, v)
}
// 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
// 尝试读取错误响应体
errorBody, _ := io.ReadAll(resp.Body)
errorMsg := string(errorBody)
if errorMsg == "" {
errorMsg = resp.Status
}
return nil, &HTTPError{
StatusCode: resp.StatusCode,
Message: errorMsg,
}
}
return io.ReadAll(resp.Body)
}
func removePathPrefix(fullPath, prefix string) (string, error) {
// 使用系统文件分隔符标准化路径
prefix = filepath.Clean(prefix) + string(filepath.Separator)
fullPath = filepath.Clean(fullPath)
// 检查路径是否有指定的前缀
if strings.HasPrefix(fullPath, prefix) {
// 返回去除前缀的路径
return fullPath[len(prefix):], nil
}
// 如果不匹配,返回原始路径(或者可以根据需要返回错误)
return "", fmt.Errorf("错误的filepath和prefix, filepath:%v, prefix:%v", fullPath, prefix)
}
func (c *HTTPClient) encodeFilepath(paths []string) []string {
var result []string
for _, path := range paths {
// 编码文件路径,使用urlencoded编码
encodedPath := url.PathEscape(path)
result = append(result, encodedPath)
}
fmt.Println("encodeFilepath result: ", result)
return result
}
// DownloadFile 下载文件到本地路径(支持大文件流式处理)
// localPath保存文件的本地路径包含文件名
// queryParams可选查询参数
func (c *HTTPClient) DownloadFile(path, localPath string, queryParams ...map[string]string) (string, error) {
// 构建完整URL
fullURL := c.BaseURL + path
// 处理查询参数
if len(queryParams) > 0 {
u, err := url.Parse(fullURL)
if err != nil {
return "", fmt.Errorf("解析URL失败: %w", err)
}
q := u.Query()
for key, value := range queryParams[0] {
q.Add(key, value)
}
u.RawQuery = q.Encode()
fullURL = u.String()
}
// 创建请求
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
// 设置请求头
for k, v := range c.Headers {
req.Header.Set(k, v)
}
// 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
// 检查状态码 (接受2xx状态码)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// 尝试读取部分错误信息(限制长度防止内存溢出)
return "", &HTTPError{
StatusCode: resp.StatusCode,
Message: resp.Status,
}
}
os.MkdirAll(localPath, 0755)
filename := extractFilenameFromDisposition(resp.Header.Get("Content-Disposition"))
if filename == "" {
filename = filepath.Join(localPath, "file.zip")
} else {
filename = filepath.Join(localPath, filename)
}
// 创建本地文件
outFile, err := os.Create(filename)
if err != nil {
return "", fmt.Errorf("创建本地文件失败: %w", err)
}
defer outFile.Close()
// 流式拷贝(避免大文件内存溢出)
_, err = io.Copy(outFile, resp.Body)
if err != nil {
// 删除可能不完整的文件
_ = os.Remove(localPath)
return "", fmt.Errorf("下载文件写入失败: %w", err)
}
return filename, nil
}
func extractFilenameFromDisposition(contentDisposition string) string {
// 尝试使用标准库的 mime.ParseMediaType
if _, params, err := mime.ParseMediaType(contentDisposition); err == nil {
if filename, exists := params["filename"]; exists && filename != "" {
return filename
}
}
return ""
}
// HTTPError 自定义HTTP错误
type HTTPError struct {
StatusCode int
Message string
}
func (e *HTTPError) Error() string {
return e.Message
}