2026-05-03 00:00:13 +08:00
|
|
|
|
package http
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"mime/multipart"
|
|
|
|
|
|
"net"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/http/cookiejar"
|
|
|
|
|
|
"net/textproto"
|
|
|
|
|
|
url2 "net/url"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
2026-05-03 00:34:29 +08:00
|
|
|
|
"sync"
|
2026-05-03 00:00:13 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
|
"apigo.cc/go/encoding"
|
|
|
|
|
|
"apigo.cc/go/file"
|
|
|
|
|
|
"apigo.cc/go/log"
|
|
|
|
|
|
"apigo.cc/go/rand"
|
|
|
|
|
|
"golang.org/x/net/http2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type Client struct {
|
|
|
|
|
|
pool *http.Client
|
2026-05-03 00:34:29 +08:00
|
|
|
|
globalHeaders map[string]string
|
|
|
|
|
|
headersMu sync.RWMutex
|
2026-05-03 00:00:13 +08:00
|
|
|
|
NoBody bool
|
|
|
|
|
|
Debug bool
|
|
|
|
|
|
DownloadPartSize int64
|
2026-05-03 00:34:29 +08:00
|
|
|
|
MaxConnsPerHost int
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Result struct {
|
|
|
|
|
|
Error error
|
|
|
|
|
|
Response *http.Response
|
|
|
|
|
|
data []byte
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Form = map[string]string
|
|
|
|
|
|
|
2026-05-03 00:34:29 +08:00
|
|
|
|
var bufferPool = sync.Pool{
|
|
|
|
|
|
New: func() any {
|
|
|
|
|
|
return new(bytes.Buffer)
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-03 00:00:13 +08:00
|
|
|
|
func NewClient(timeout time.Duration) *Client {
|
|
|
|
|
|
if timeout < time.Millisecond && timeout > 0 {
|
|
|
|
|
|
timeout *= time.Millisecond
|
|
|
|
|
|
}
|
|
|
|
|
|
jar, _ := cookiejar.New(nil)
|
|
|
|
|
|
return &Client{
|
|
|
|
|
|
pool: &http.Client{
|
|
|
|
|
|
Timeout: timeout,
|
|
|
|
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
|
|
|
|
return http.ErrUseLastResponse
|
|
|
|
|
|
},
|
|
|
|
|
|
Jar: jar,
|
|
|
|
|
|
},
|
2026-05-03 00:34:29 +08:00
|
|
|
|
globalHeaders: map[string]string{},
|
2026-05-03 00:00:13 +08:00
|
|
|
|
DownloadPartSize: 4194304,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewClientH2C(timeout time.Duration) *Client {
|
|
|
|
|
|
if timeout < time.Millisecond && timeout > 0 {
|
|
|
|
|
|
timeout *= time.Millisecond
|
|
|
|
|
|
}
|
|
|
|
|
|
jar, _ := cookiejar.New(nil)
|
|
|
|
|
|
clientConfig := &http.Client{
|
|
|
|
|
|
Transport: &http2.Transport{
|
|
|
|
|
|
AllowHTTP: true,
|
|
|
|
|
|
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
|
|
|
|
|
return net.Dial(network, addr)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
|
|
|
|
return http.ErrUseLastResponse
|
|
|
|
|
|
},
|
|
|
|
|
|
Timeout: timeout,
|
|
|
|
|
|
Jar: jar,
|
|
|
|
|
|
}
|
|
|
|
|
|
return &Client{
|
|
|
|
|
|
pool: clientConfig,
|
2026-05-03 00:34:29 +08:00
|
|
|
|
globalHeaders: map[string]string{},
|
2026-05-03 00:00:13 +08:00
|
|
|
|
DownloadPartSize: 4194304,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) GetRawClient() *http.Client {
|
|
|
|
|
|
return client.pool
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) EnableRedirect() {
|
|
|
|
|
|
client.pool.CheckRedirect = nil
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) SetGlobalHeader(key, value string) {
|
|
|
|
|
|
client.headersMu.Lock()
|
|
|
|
|
|
defer client.headersMu.Unlock()
|
|
|
|
|
|
if value == "" {
|
|
|
|
|
|
delete(client.globalHeaders, key)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
} else {
|
2026-05-05 17:47:06 +08:00
|
|
|
|
client.globalHeaders[key] = value
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) GetGlobalHeader(key string) string {
|
|
|
|
|
|
client.headersMu.RLock()
|
|
|
|
|
|
defer client.headersMu.RUnlock()
|
|
|
|
|
|
return client.globalHeaders[key]
|
2026-05-03 00:34:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Destroy() {
|
|
|
|
|
|
if client.pool != nil {
|
|
|
|
|
|
client.pool.CloseIdleConnections()
|
|
|
|
|
|
client.pool = nil
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Get(url string, headers ...string) *Result {
|
|
|
|
|
|
return client.Do("GET", url, nil, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Post(url string, data any, headers ...string) *Result {
|
|
|
|
|
|
return client.Do("POST", url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Put(url string, data any, headers ...string) *Result {
|
|
|
|
|
|
return client.Do("PUT", url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Delete(url string, data any, headers ...string) *Result {
|
|
|
|
|
|
return client.Do("DELETE", url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Head(url string, headers ...string) *Result {
|
|
|
|
|
|
return client.Do("HEAD", url, nil, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) DoByRequest(request *http.Request, method, url string, data any, settedHeaders ...string) *Result {
|
|
|
|
|
|
return client.doByRequest(false, request, method, url, data, settedHeaders...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) ManualDoByRequest(request *http.Request, method, url string, data any, settedHeaders ...string) *Result {
|
|
|
|
|
|
return client.doByRequest(true, request, method, url, data, settedHeaders...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) doByRequest(manualDo bool, request *http.Request, method, url string, data any, settedHeaders ...string) *Result {
|
2026-05-03 00:34:29 +08:00
|
|
|
|
headers := make([]string, 0, len(RelayHeaders)*2+len(settedHeaders)+4)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 续传指定的头
|
2026-05-05 17:47:06 +08:00
|
|
|
|
for _, headerName := range RelayHeaders {
|
|
|
|
|
|
if value := request.Header.Get(headerName); value != "" {
|
|
|
|
|
|
headers = append(headers, headerName, value)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 续传 X-Forwarded-For
|
|
|
|
|
|
xForwardFor := request.Header.Get(HeaderForwardedFor)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
remoteIP, _, err := net.SplitHostPort(request.RemoteAddr)
|
|
|
|
|
|
if err != nil {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
remoteIP = request.RemoteAddr
|
|
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if xForwardFor != "" {
|
|
|
|
|
|
xForwardFor = remoteIP + ", " + xForwardFor
|
|
|
|
|
|
} else {
|
|
|
|
|
|
xForwardFor = remoteIP
|
|
|
|
|
|
}
|
2026-05-03 00:34:29 +08:00
|
|
|
|
headers = append(headers, HeaderForwardedFor, xForwardFor)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理请求唯一编号
|
2026-05-03 00:34:29 +08:00
|
|
|
|
foundID := false
|
|
|
|
|
|
for i := 1; i < len(headers); i += 2 {
|
|
|
|
|
|
if headers[i-1] == HeaderRequestID {
|
|
|
|
|
|
foundID = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-03 00:34:29 +08:00
|
|
|
|
if !foundID {
|
|
|
|
|
|
headers = append(headers, HeaderRequestID, string(encoding.Hex(rand.Bytes(16))))
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-03 00:34:29 +08:00
|
|
|
|
headers = append(headers, settedHeaders...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
|
|
|
|
|
|
if manualDo {
|
2026-05-05 17:47:06 +08:00
|
|
|
|
return client.ManualDo(method, url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
return client.Do(method, url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Do(method, url string, data any, headers ...string) *Result {
|
|
|
|
|
|
return client.do(true, method, url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) ManualDo(method, url string, data any, headers ...string) *Result {
|
|
|
|
|
|
return client.do(false, method, url, data, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type downloadRange struct {
|
|
|
|
|
|
Start int64
|
|
|
|
|
|
End int64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-03 00:34:29 +08:00
|
|
|
|
type offsetWriter struct {
|
|
|
|
|
|
fp *os.File
|
|
|
|
|
|
offset int64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (w *offsetWriter) Write(p []byte) (n int, err error) {
|
|
|
|
|
|
n, err = w.fp.WriteAt(p, w.offset)
|
|
|
|
|
|
w.offset += int64(n)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) downloadPart(fp *os.File, task *downloadRange, url string, headers ...string) (int64, error) {
|
2026-05-03 00:34:29 +08:00
|
|
|
|
partHeaders := make([]string, len(headers))
|
|
|
|
|
|
copy(partHeaders, headers)
|
|
|
|
|
|
partHeaders[len(partHeaders)-1] = fmt.Sprintf("bytes=%d-%d", task.Start, task.End)
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
result := client.ManualDo("GET", url, nil, partHeaders...)
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
|
return 0, result.Error
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
defer result.Response.Body.Close()
|
|
|
|
|
|
return io.Copy(&offsetWriter{fp: fp, offset: task.Start}, result.Response.Body)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) Download(filename, url string, callback func(start, end int64, ok bool, finished, total int64), headers ...string) (*Result, error) {
|
|
|
|
|
|
resultHead := client.Head(url, headers...)
|
|
|
|
|
|
if resultHead.Error != nil {
|
|
|
|
|
|
return resultHead, resultHead.Error
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
total := resultHead.Response.ContentLength
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if total > 0 {
|
|
|
|
|
|
tasks := make([]downloadRange, 0)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
for i := int64(0); i < total; i += client.DownloadPartSize {
|
|
|
|
|
|
end := i + client.DownloadPartSize - 1
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if end >= total {
|
|
|
|
|
|
end = total - 1
|
|
|
|
|
|
}
|
|
|
|
|
|
tasks = append(tasks, downloadRange{i, end})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
file.EnsureParentDir(filename)
|
|
|
|
|
|
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer fp.Close()
|
|
|
|
|
|
|
|
|
|
|
|
headers = append(headers, "Range", "")
|
2026-05-03 00:34:29 +08:00
|
|
|
|
var finished int64
|
|
|
|
|
|
var mu sync.Mutex
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
errChan := make(chan error, len(tasks))
|
|
|
|
|
|
|
|
|
|
|
|
// 限制并发度,默认为 4,可以通过 Client 设置
|
|
|
|
|
|
concurrency := 4
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if client.MaxConnsPerHost > 0 {
|
|
|
|
|
|
concurrency = client.MaxConnsPerHost
|
2026-05-03 00:34:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
sem := make(chan struct{}, concurrency)
|
|
|
|
|
|
|
2026-05-03 00:00:13 +08:00
|
|
|
|
for _, task := range tasks {
|
2026-05-03 00:34:29 +08:00
|
|
|
|
wg.Add(1)
|
|
|
|
|
|
go func(t downloadRange) {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
sem <- struct{}{}
|
|
|
|
|
|
defer func() { <-sem }()
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
n, err := client.downloadPart(fp, &t, url, headers...)
|
2026-05-03 00:34:29 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 重试一次
|
2026-05-05 17:47:06 +08:00
|
|
|
|
n, err = client.downloadPart(fp, &t, url, headers...)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-03 00:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
|
|
finished += n
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if callback != nil {
|
2026-05-03 00:34:29 +08:00
|
|
|
|
callback(t.Start, t.End, err == nil, finished, total)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-03 00:34:29 +08:00
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
errChan <- err
|
|
|
|
|
|
}
|
|
|
|
|
|
}(task)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
close(errChan)
|
|
|
|
|
|
|
|
|
|
|
|
if len(errChan) > 0 {
|
|
|
|
|
|
return nil, <-errChan
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if finished < total {
|
|
|
|
|
|
return nil, errors.New("download file failed: incomplete")
|
|
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
return resultHead, nil
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
result := client.ManualDo("GET", url, nil, headers...)
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
|
return result, result.Error
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
defer result.Response.Body.Close()
|
2026-05-03 00:00:13 +08:00
|
|
|
|
file.EnsureParentDir(filename)
|
|
|
|
|
|
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
|
|
|
|
if err != nil {
|
2026-05-05 17:47:06 +08:00
|
|
|
|
return result, err
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
defer fp.Close()
|
2026-05-05 17:47:06 +08:00
|
|
|
|
_, err = io.Copy(fp, result.Response.Body)
|
|
|
|
|
|
return result, err
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) PostMultipart(url string, formData map[string]string, files map[string]any, headers ...string) (*Result, []error) {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
errs := make([]error, 0)
|
2026-05-03 00:34:29 +08:00
|
|
|
|
buf := bufferPool.Get().(*bytes.Buffer)
|
|
|
|
|
|
buf.Reset()
|
|
|
|
|
|
defer bufferPool.Put(buf)
|
|
|
|
|
|
|
|
|
|
|
|
writer := multipart.NewWriter(buf)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
|
|
|
|
|
|
if formData != nil {
|
2026-05-05 17:47:06 +08:00
|
|
|
|
for key, value := range formData {
|
|
|
|
|
|
if err := writer.WriteField(key, value); err != nil {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if files != nil {
|
2026-05-05 17:47:06 +08:00
|
|
|
|
for key, value := range files {
|
|
|
|
|
|
if filename, ok := value.(string); ok && file.Exists(filename) {
|
|
|
|
|
|
var reader io.Reader
|
|
|
|
|
|
var closer io.Closer
|
|
|
|
|
|
if mf := file.ReadFileFromMemory(filename); mf != nil {
|
|
|
|
|
|
reader = bytes.NewReader(mf.GetData())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if fp, err := os.Open(filename); err == nil {
|
|
|
|
|
|
reader = fp
|
|
|
|
|
|
closer = fp
|
2026-05-03 00:00:13 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
errs = append(errs, err)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if part, err := writer.CreateFormFile(key, filepath.Base(filename)); err == nil {
|
|
|
|
|
|
if _, err = io.Copy(part, reader); err != nil {
|
|
|
|
|
|
errs = append(errs, err)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
|
|
|
|
|
|
if closer != nil {
|
|
|
|
|
|
_ = closer.Close()
|
|
|
|
|
|
}
|
2026-05-03 00:00:13 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
h := make(textproto.MIMEHeader)
|
2026-05-03 00:34:29 +08:00
|
|
|
|
var dataBytes []byte
|
2026-05-05 17:47:06 +08:00
|
|
|
|
switch t := value.(type) {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
case io.Reader:
|
2026-05-03 00:34:29 +08:00
|
|
|
|
dataBytes, _ = io.ReadAll(t)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, key, key))
|
2026-05-03 00:00:13 +08:00
|
|
|
|
h.Set("Content-Type", "application/octet-stream")
|
|
|
|
|
|
case []byte:
|
2026-05-03 00:34:29 +08:00
|
|
|
|
dataBytes = t
|
2026-05-05 17:47:06 +08:00
|
|
|
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, key, key))
|
2026-05-03 00:00:13 +08:00
|
|
|
|
h.Set("Content-Type", "application/octet-stream")
|
|
|
|
|
|
case string:
|
2026-05-03 00:34:29 +08:00
|
|
|
|
dataBytes = []byte(t)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s.txt"`, key, key))
|
2026-05-03 00:00:13 +08:00
|
|
|
|
h.Set("Content-Type", "text/plain")
|
|
|
|
|
|
default:
|
2026-05-05 17:47:06 +08:00
|
|
|
|
dataBytes = cast.As(cast.ToJSONBytes(value))
|
|
|
|
|
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s.json"`, key, key))
|
2026-05-03 00:00:13 +08:00
|
|
|
|
h.Set("Content-Type", "application/json")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if part, err := writer.CreatePart(h); err == nil {
|
2026-05-03 00:34:29 +08:00
|
|
|
|
if _, err = part.Write(dataBytes); err != nil {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := writer.Close(); err != nil {
|
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
return nil, errs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
headers = append(headers, "Content-Type", writer.FormDataContentType())
|
2026-05-05 17:47:06 +08:00
|
|
|
|
result := client.Post(url, buf.Bytes(), headers...)
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
|
errs = append(errs, result.Error)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
return result, errs
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (client *Client) do(fetchBody bool, method, url string, data any, headers ...string) *Result {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
var req *http.Request
|
|
|
|
|
|
var err error
|
|
|
|
|
|
contentType := ""
|
|
|
|
|
|
contentLength := 0
|
|
|
|
|
|
var reader io.Reader
|
|
|
|
|
|
|
|
|
|
|
|
if data != nil {
|
|
|
|
|
|
switch t := data.(type) {
|
|
|
|
|
|
case io.Reader:
|
|
|
|
|
|
reader = t
|
|
|
|
|
|
case []byte:
|
|
|
|
|
|
reader = bytes.NewReader(t)
|
|
|
|
|
|
contentLength = len(t)
|
|
|
|
|
|
case string:
|
|
|
|
|
|
reader = strings.NewReader(t)
|
|
|
|
|
|
contentLength = len(t)
|
|
|
|
|
|
case url2.Values:
|
|
|
|
|
|
encoded := t.Encode()
|
|
|
|
|
|
reader = strings.NewReader(encoded)
|
|
|
|
|
|
contentType = "application/x-www-form-urlencoded"
|
|
|
|
|
|
contentLength = len(encoded)
|
|
|
|
|
|
case map[string][]string:
|
|
|
|
|
|
values := url2.Values(t)
|
|
|
|
|
|
encoded := values.Encode()
|
|
|
|
|
|
reader = strings.NewReader(encoded)
|
|
|
|
|
|
contentType = "application/x-www-form-urlencoded"
|
|
|
|
|
|
contentLength = len(encoded)
|
|
|
|
|
|
case map[string]string:
|
|
|
|
|
|
values := url2.Values{}
|
|
|
|
|
|
for k, v := range t {
|
|
|
|
|
|
values.Set(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
encoded := values.Encode()
|
|
|
|
|
|
reader = strings.NewReader(encoded)
|
|
|
|
|
|
contentType = "application/x-www-form-urlencoded"
|
|
|
|
|
|
contentLength = len(encoded)
|
|
|
|
|
|
default:
|
|
|
|
|
|
bytesData, _ := cast.ToJSONBytes(data)
|
|
|
|
|
|
if len(bytesData) > 0 && string(bytesData) != "null" {
|
|
|
|
|
|
reader = bytes.NewReader(bytesData)
|
|
|
|
|
|
contentType = "application/json"
|
|
|
|
|
|
contentLength = len(bytesData)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req, err = http.NewRequest(method, url, reader)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &Result{Error: err}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if contentType != "" {
|
|
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
|
|
}
|
|
|
|
|
|
if contentLength > 0 {
|
|
|
|
|
|
req.Header.Set("Content-Length", cast.String(contentLength))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for i := 1; i < len(headers); i += 2 {
|
|
|
|
|
|
if headers[i-1] == "Host" {
|
|
|
|
|
|
req.Host = headers[i]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
req.Header.Set(headers[i-1], headers[i])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
client.headersMu.RLock()
|
|
|
|
|
|
for k, v := range client.globalHeaders {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
req.Header.Set(k, v)
|
|
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
client.headersMu.RUnlock()
|
2026-05-03 00:00:13 +08:00
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if client.Debug {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
log.DefaultLogger.Info("http request", "method", req.Method, "url", req.URL.String(), "headers", req.Header)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
res, err := client.pool.Do(req)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &Result{Error: err}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if res.ContentLength == -1 {
|
|
|
|
|
|
res.ContentLength = cast.Int64(res.Header.Get("Content-Length"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if !fetchBody || client.NoBody {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return &Result{Response: res}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
bodyBytes, err := io.ReadAll(res.Body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &Result{Error: err, Response: res}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if client.Debug {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
log.DefaultLogger.Info("http response", "status", res.StatusCode, "len", len(bodyBytes))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &Result{data: bodyBytes, Response: res}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) Save(filename string) error {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
file.EnsureParentDir(filename)
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if result.data != nil {
|
|
|
|
|
|
return file.WriteBytes(filename, result.data)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if result.Response != nil && result.Response.Body != nil {
|
|
|
|
|
|
defer result.Response.Body.Close()
|
2026-05-03 00:00:13 +08:00
|
|
|
|
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer fp.Close()
|
2026-05-05 17:47:06 +08:00
|
|
|
|
_, err = io.Copy(fp, result.Response.Body)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return errors.New("no data to save")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) String() string {
|
|
|
|
|
|
return string(result.data)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) Bytes() []byte {
|
|
|
|
|
|
return result.data
|
2026-05-03 00:00:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) Map() map[string]any {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
var m map[string]any
|
2026-05-05 17:47:06 +08:00
|
|
|
|
_ = result.To(&m)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return m
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) Slice() []any {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
var a []any
|
2026-05-05 17:47:06 +08:00
|
|
|
|
_ = result.To(&a)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return a
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func (result *Result) To(v any) error {
|
|
|
|
|
|
if result.data == nil {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return errors.New("no data")
|
|
|
|
|
|
}
|
2026-05-05 17:47:06 +08:00
|
|
|
|
err := cast.UnmarshalJSON(result.data, v)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 如果 cast 直接解不出来,尝试通过 convert 做深度映射(处理 struct 字段匹配等)
|
|
|
|
|
|
var tmp any
|
2026-05-05 17:47:06 +08:00
|
|
|
|
if err2 := cast.UnmarshalJSON(result.data, &tmp); err2 == nil {
|
|
|
|
|
|
cast.Convert(v, tmp)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-03 12:19:19 +08:00
|
|
|
|
// To 使用泛型获取结果
|
2026-05-05 17:47:06 +08:00
|
|
|
|
func To[T any](result *Result) (T, error) {
|
2026-05-03 00:00:13 +08:00
|
|
|
|
var v T
|
2026-05-05 17:47:06 +08:00
|
|
|
|
err := result.To(&v)
|
2026-05-03 00:00:13 +08:00
|
|
|
|
return v, err
|
|
|
|
|
|
}
|