starnet/curl.go

1640 lines
40 KiB
Go
Raw Normal View History

2020-07-20 11:17:29 +08:00
package starnet
import (
"bytes"
2022-03-11 09:29:09 +08:00
"context"
2022-03-14 15:43:56 +08:00
"crypto/tls"
2024-08-08 22:03:10 +08:00
"encoding/json"
2020-07-20 11:17:29 +08:00
"fmt"
"io"
2024-08-08 22:03:10 +08:00
"mime/multipart"
2020-07-20 11:17:29 +08:00
"net"
"net/http"
"net/url"
2020-07-21 09:20:02 +08:00
"os"
2024-08-08 22:03:10 +08:00
"strconv"
2020-10-19 21:04:56 +08:00
"strings"
2025-04-28 13:19:45 +08:00
"sync"
2020-07-20 11:17:29 +08:00
"time"
)
2020-10-19 21:04:56 +08:00
const (
HEADER_FORM_URLENCODE = `application/x-www-form-urlencoded`
HEADER_FORM_DATA = `multipart/form-data`
HEADER_JSON = `application/json`
2023-02-11 17:15:01 +08:00
HEADER_PLAIN = `text/plain`
2020-10-19 21:04:56 +08:00
)
2024-08-08 22:03:10 +08:00
var (
DefaultDialTimeout = 5 * time.Second
DefaultTimeout = 10 * time.Second
DefaultFetchRespBody = false
)
func UrlEncodeRaw(str string) string {
strs := strings.Replace(url.QueryEscape(str), "+", "%20", -1)
return strs
}
func UrlEncode(str string) string {
return url.QueryEscape(str)
}
func UrlDecode(str string) (string, error) {
return url.QueryUnescape(str)
}
func BuildQuery(queryData map[string]string) string {
query := url.Values{}
for k, v := range queryData {
query.Add(k, v)
}
return query.Encode()
}
// BuildPostForm takes a map of string keys and values, converts it into a URL-encoded query string,
// and then converts that string into a byte slice. This function is useful for preparing data for HTTP POST requests,
// where the server expects the request body to be URL-encoded form data.
//
// Parameters:
// queryMap: A map where the key-value pairs represent the form data to be sent in the HTTP POST request.
//
// Returns:
// A byte slice representing the URL-encoded form data.
func BuildPostForm(queryMap map[string]string) []byte {
return []byte(BuildQuery(queryMap))
}
func Get(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "GET", opts...).Do()
}
func Post(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "POST", opts...).Do()
}
func Options(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "OPTIONS", opts...).Do()
}
func Put(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "PUT", opts...).Do()
}
func Delete(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "DELETE", opts...).Do()
}
func Head(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "HEAD", opts...).Do()
}
func Patch(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "PATCH", opts...).Do()
}
func Trace(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "TRACE", opts...).Do()
}
func Connect(uri string, opts ...RequestOpt) (*Response, error) {
return NewSimpleRequest(uri, "CONNECT", opts...).Do()
2020-07-21 09:20:02 +08:00
}
2022-03-14 15:43:56 +08:00
2020-07-20 11:17:29 +08:00
type Request struct {
2024-08-08 22:03:10 +08:00
ctx context.Context
uri string
method string
errInfo error
2022-03-14 15:43:56 +08:00
RequestOpts
}
2025-04-28 13:19:45 +08:00
func (r *Request) Clone() *Request {
clonedRequest := &Request{
ctx: r.ctx,
uri: r.uri,
method: r.method,
errInfo: r.errInfo,
RequestOpts: RequestOpts{
headers: CloneHeader(r.headers),
cookies: CloneCookies(r.cookies),
bodyFormData: CloneStringMapSlice(r.bodyFormData),
bodyFileData: CloneFiles(r.bodyFileData),
queries: CloneStringMapSlice(r.queries),
bodyDataBytes: CloneByteSlice(r.bodyDataBytes),
proxy: r.proxy,
timeout: r.timeout,
dialTimeout: r.dialTimeout,
alreadyApply: r.alreadyApply,
disableRedirect: r.disableRedirect,
doRawRequest: r.doRawRequest,
doRawClient: r.doRawClient,
doRawTransport: r.doRawTransport,
skipTLSVerify: r.skipTLSVerify,
autoFetchRespBody: r.autoFetchRespBody,
customIP: CloneStringSlice(r.customIP),
alreadySetLookUpIPfn: r.alreadySetLookUpIPfn,
lookUpIPfn: r.lookUpIPfn,
customDNS: CloneStringSlice(r.customDNS),
basicAuth: r.basicAuth,
autoCalcContentLength: r.autoCalcContentLength,
},
}
// 手动深拷贝嵌套引用类型
if r.bodyDataReader != nil {
clonedRequest.bodyDataReader = r.bodyDataReader
}
if r.FileUploadRecallFn != nil {
clonedRequest.FileUploadRecallFn = r.FileUploadRecallFn
}
// 对于 tlsConfig 类型,需要手动复制
if r.tlsConfig != nil {
clonedRequest.tlsConfig = CloneTLSConfig(r.tlsConfig)
}
// 对于 http.Transport需要进行手动复制
if r.transport != nil {
clonedRequest.transport = CloneTransport(r.transport)
}
return clonedRequest
}
// CloneHeader 复制 http.Header
func CloneHeader(original http.Header) http.Header {
newHeader := make(http.Header)
for key, values := range original {
copiedValues := make([]string, len(values))
copy(copiedValues, values)
newHeader[key] = copiedValues
}
return newHeader
}
// CloneCookies 复制 []*http.Cookie
func CloneCookies(original []*http.Cookie) []*http.Cookie {
cloned := make([]*http.Cookie, len(original))
for i, cookie := range original {
cloned[i] = &http.Cookie{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Expires: cookie.Expires,
RawExpires: cookie.RawExpires,
MaxAge: cookie.MaxAge,
Secure: cookie.Secure,
HttpOnly: cookie.HttpOnly,
SameSite: cookie.SameSite,
Raw: cookie.Raw,
Unparsed: append([]string(nil), cookie.Unparsed...),
}
}
return cloned
}
// CloneStringMapSlice 复制 map[string][]string
func CloneStringMapSlice(original map[string][]string) map[string][]string {
newMap := make(map[string][]string)
for key, values := range original {
copiedValues := make([]string, len(values))
copy(copiedValues, values)
newMap[key] = copiedValues
}
return newMap
}
// CloneFiles 复制 []RequestFile
func CloneFiles(original []RequestFile) []RequestFile {
newFiles := make([]RequestFile, len(original))
copy(newFiles, original)
return newFiles
}
// CloneByteSlice 复制 []byte
func CloneByteSlice(original []byte) []byte {
if original == nil {
return nil
}
newSlice := make([]byte, len(original))
copy(newSlice, original)
return newSlice
}
// CloneStringSlice 复制 []string
func CloneStringSlice(original []string) []string {
newSlice := make([]string, len(original))
copy(newSlice, original)
return newSlice
}
// CloneTLSConfig 复制 tls.Config
func CloneTLSConfig(original *tls.Config) *tls.Config {
newConfig := &tls.Config{
Rand: original.Rand,
Time: original.Time,
Certificates: append([]tls.Certificate(nil), original.Certificates...),
NameToCertificate: original.NameToCertificate,
GetCertificate: original.GetCertificate,
GetClientCertificate: original.GetClientCertificate,
GetConfigForClient: original.GetConfigForClient,
VerifyPeerCertificate: original.VerifyPeerCertificate,
VerifyConnection: original.VerifyConnection,
RootCAs: original.RootCAs,
NextProtos: append([]string(nil), original.NextProtos...),
ServerName: original.ServerName,
ClientAuth: original.ClientAuth,
ClientCAs: original.ClientCAs,
InsecureSkipVerify: original.InsecureSkipVerify,
CipherSuites: append([]uint16(nil), original.CipherSuites...),
PreferServerCipherSuites: original.PreferServerCipherSuites,
SessionTicketsDisabled: original.SessionTicketsDisabled,
SessionTicketKey: original.SessionTicketKey,
ClientSessionCache: original.ClientSessionCache,
MinVersion: original.MinVersion,
MaxVersion: original.MaxVersion,
CurvePreferences: append([]tls.CurveID(nil), original.CurvePreferences...),
DynamicRecordSizingDisabled: original.DynamicRecordSizingDisabled,
Renegotiation: original.Renegotiation,
KeyLogWriter: original.KeyLogWriter,
}
return newConfig
}
// CloneTransport 复制 http.Transport
func CloneTransport(original *http.Transport) *http.Transport {
newTransport := &http.Transport{
Proxy: original.Proxy,
DialContext: original.DialContext,
Dial: original.Dial,
DialTLS: original.DialTLS,
TLSClientConfig: original.TLSClientConfig,
TLSHandshakeTimeout: original.TLSHandshakeTimeout,
DisableKeepAlives: original.DisableKeepAlives,
DisableCompression: original.DisableCompression,
MaxIdleConns: original.MaxIdleConns,
MaxIdleConnsPerHost: original.MaxIdleConnsPerHost,
IdleConnTimeout: original.IdleConnTimeout,
ResponseHeaderTimeout: original.ResponseHeaderTimeout,
ExpectContinueTimeout: original.ExpectContinueTimeout,
TLSNextProto: original.TLSNextProto,
ProxyConnectHeader: original.ProxyConnectHeader,
MaxResponseHeaderBytes: original.MaxResponseHeaderBytes,
WriteBufferSize: original.WriteBufferSize,
ReadBufferSize: original.ReadBufferSize,
}
return newTransport
}
2024-08-08 22:03:10 +08:00
func (r *Request) Method() string {
return r.method
}
func (r *Request) SetMethod(method string) error {
method = strings.ToUpper(method)
if !validMethod(method) {
return fmt.Errorf("invalid method: %s", method)
}
r.method = method
r.rawRequest.Method = method
return nil
}
func (r *Request) SetMethodNoError(method string) *Request {
r.SetMethod(method)
return r
}
func (r *Request) Uri() string {
return r.uri
}
func (r *Request) SetUri(uri string) error {
u, err := url.Parse(uri)
if err != nil {
return fmt.Errorf("parse uri error: %s", err)
}
r.uri = uri
u.Host = removeEmptyPort(u.Host)
r.rawRequest.Host = u.Host
r.rawRequest.URL = u
return nil
}
func (r *Request) SetUriNoError(uri string) *Request {
r.SetUri(uri)
return r
}
func (r *Request) RawRequest() *http.Request {
return r.rawRequest
}
func (r *Request) SetRawRequest(rawRequest *http.Request) *Request {
r.rawRequest = rawRequest
return r
}
func (r *Request) RawClient() *http.Client {
return r.rawClient
}
func (r *Request) SetRawClient(rawClient *http.Client) *Request {
r.rawClient = rawClient
return r
}
func (r *Request) Do() (*Response, error) {
return Curl(r)
}
func (r *Request) Get() (*Response, error) {
err := r.SetMethod("GET")
if err != nil {
return nil, err
}
return Curl(r)
}
func (r *Request) Post(data []byte) (*Response, error) {
err := r.SetMethod("POST")
if err != nil {
return nil, err
}
r.bodyDataBytes = data
r.bodyDataReader = nil
return Curl(r)
}
2022-03-14 15:43:56 +08:00
type RequestOpts struct {
2024-08-08 22:03:10 +08:00
rawRequest *http.Request
rawClient *http.Client
alreadyApply bool
bodyDataBytes []byte
bodyDataReader io.Reader
bodyFormData map[string][]string
bodyFileData []RequestFile
//以上优先度为 bodyDataReader> bodyDataBytes > bodyFormData > bodyFileData
FileUploadRecallFn func(filename string, upPos int64, total int64)
proxy string
timeout time.Duration
dialTimeout time.Duration
headers http.Header
cookies []*http.Cookie
transport *http.Transport
queries map[string][]string
disableRedirect bool
//doRawRequest=true 不对request修改直接发送
doRawRequest bool
//doRawClient=true 不对http client修改直接发送
doRawClient bool
//doRawTransPort=true 不对http transport修改直接发送
doRawTransport bool
skipTLSVerify bool
tlsConfig *tls.Config
autoFetchRespBody bool
customIP []string
alreadySetLookUpIPfn bool
lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)
customDNS []string
basicAuth [2]string
autoCalcContentLength bool
}
func (r *Request) AutoCalcContentLength() bool {
return r.autoCalcContentLength
}
// SetAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
// also the memory usage will be higher
// Note that this function will not work if doRawRequest is true
func (r *Request) SetAutoCalcContentLength(autoCalcContentLength bool) *Request {
r.autoCalcContentLength = autoCalcContentLength
return r
}
// BasicAuth returns the username and password provided in the request's Authorization header.
func (r *RequestOpts) BasicAuth() (string, string) {
return r.basicAuth[0], r.basicAuth[1]
}
// SetBasicAuth sets the request's Authorization header to use HTTP Basic Authentication with the provided username and password.
// Note: If doRawRequest is true, this function will nolonger work
func (r *RequestOpts) SetBasicAuth(username, password string) *RequestOpts {
r.basicAuth = [2]string{username, password}
return r
}
func (r *Request) DoRawTransport() bool {
return r.doRawTransport
}
func (r *Request) SetDoRawTransport(doRawTransport bool) *Request {
r.doRawTransport = doRawTransport
return r
}
func (r *Request) CustomDNS() []string {
return r.customDNS
}
// Note: if LookUpIPfn is set, this function will not be used
func (r *Request) SetCustomDNS(customDNS []string) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
}
r.customDNS = customDNS
return nil
}
// Note: if LookUpIPfn is set, this function will not be used
func (r *Request) SetCustomDNSNoError(customDNS []string) *Request {
r.SetCustomDNS(customDNS)
return r
}
// Note: if LookUpIPfn is set, this function will not be used
func (r *Request) AddCustomDNS(customDNS []string) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
}
r.customDNS = customDNS
return nil
}
// Note: if LookUpIPfn is set, this function will not be used
func (r *Request) AddCustomDNSNoError(customDNS []string) *Request {
r.AddCustomDNS(customDNS)
return r
}
func (r *Request) LookUpIPfn() func(ctx context.Context, host string) ([]net.IPAddr, error) {
return r.lookUpIPfn
}
func (r *Request) SetLookUpIPfn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) *Request {
if lookUpIPfn == nil {
r.alreadySetLookUpIPfn = false
r.lookUpIPfn = net.DefaultResolver.LookupIPAddr
return r
}
r.lookUpIPfn = lookUpIPfn
r.alreadySetLookUpIPfn = true
return r
}
func (r *Request) CustomHostIP() []string {
return r.customIP
}
func (r *Request) SetCustomHostIP(customIP []string) *Request {
r.customIP = customIP
return r
}
func (r *Request) AddCustomHostIP(customIP string) *Request {
r.customIP = append(r.customIP, customIP)
return r
}
func (r *Request) BodyDataBytes() []byte {
return r.bodyDataBytes
}
func (r *Request) SetBodyDataBytes(bodyDataBytes []byte) *Request {
r.bodyDataBytes = bodyDataBytes
return r
}
func (r *Request) BodyDataReader() io.Reader {
return r.bodyDataReader
}
func (r *Request) SetBodyDataReader(bodyDataReader io.Reader) *Request {
r.bodyDataReader = bodyDataReader
return r
}
func (r *Request) BodyFormData() map[string][]string {
return r.bodyFormData
}
func (r *Request) SetBodyFormData(bodyFormData map[string][]string) *Request {
r.bodyFormData = bodyFormData
return r
}
func (r *Request) SetFormData(bodyFormData map[string][]string) *Request {
return r.SetBodyFormData(bodyFormData)
}
func (r *Request) AddFormMapData(bodyFormData map[string]string) *Request {
for k, v := range bodyFormData {
r.bodyFormData[k] = append(r.bodyFormData[k], v)
}
return r
}
func (r *Request) AddFormData(k, v string) *Request {
r.bodyFormData[k] = append(r.bodyFormData[k], v)
return r
}
func (r *Request) BodyFileData() []RequestFile {
return r.bodyFileData
}
func (r *Request) SetBodyFileData(bodyFileData []RequestFile) *Request {
r.bodyFileData = bodyFileData
return r
}
func (r *Request) Proxy() string {
return r.proxy
}
func (r *Request) SetProxy(proxy string) *Request {
r.proxy = proxy
return r
}
func (r *Request) Timeout() time.Duration {
return r.timeout
}
func (r *Request) SetTimeout(timeout time.Duration) *Request {
r.timeout = timeout
return r
}
func (r *Request) DialTimeout() time.Duration {
return r.dialTimeout
}
func (r *Request) SetDialTimeout(dialTimeout time.Duration) *Request {
r.dialTimeout = dialTimeout
return r
}
func (r *Request) Headers() http.Header {
return r.headers
}
func (r *Request) SetHeaders(headers http.Header) *Request {
r.headers = headers
return r
}
func (r *Request) AddHeader(key, val string) *Request {
r.headers.Add(key, val)
return r
}
func (r *Request) SetHeader(key, val string) *Request {
r.headers.Set(key, val)
return r
}
func (r *Request) SetContentType(ct string) *Request {
r.headers.Set("Content-Type", ct)
return r
}
func (r *Request) SetUserAgent(ua string) *Request {
r.headers.Set("User-Agent", ua)
return r
}
func (r *Request) DeleteHeader(key string) *Request {
r.headers.Del(key)
return r
}
func (r *Request) Cookies() []*http.Cookie {
return r.cookies
}
func (r *Request) SetCookies(cookies []*http.Cookie) *Request {
r.cookies = cookies
return r
}
func (r *Request) Transport() *http.Transport {
return r.transport
}
func (r *Request) SetTransport(transport *http.Transport) *Request {
r.transport = transport
return r
}
func (r *Request) Queries() map[string][]string {
return r.queries
}
func (r *Request) SetQueries(queries map[string][]string) *Request {
r.queries = queries
return r
}
func (r *Request) AddQueries(queries map[string]string) *Request {
for k, v := range queries {
r.queries[k] = append(r.queries[k], v)
}
return r
}
func (r *Request) AddQuery(key, value string) *Request {
r.queries[key] = append(r.queries[key], value)
return r
}
func (r *Request) DelQueryKv(key, value string) *Request {
if _, ok := r.queries[key]; !ok {
return r
}
for i, v := range r.queries[key] {
if v == value {
r.queries[key] = append(r.queries[key][:i], r.queries[key][i+1:]...)
}
}
return r
}
func (r *Request) DelQuery(key string) *Request {
if _, ok := r.queries[key]; !ok {
return r
}
delete(r.queries, key)
return r
}
func (r *Request) DisableRedirect() bool {
return r.disableRedirect
}
func (r *Request) SetDisableRedirect(disableRedirect bool) *Request {
r.disableRedirect = disableRedirect
return r
}
func (r *Request) DoRawRequest() bool {
return r.doRawRequest
}
func (r *Request) SetDoRawRequest(doRawRequest bool) *Request {
r.doRawRequest = doRawRequest
return r
}
func (r *Request) DoRawClient() bool {
return r.doRawClient
}
func (r *Request) SetDoRawClient(doRawClient bool) *Request {
r.doRawClient = doRawClient
return r
}
func (r *RequestOpts) SkipTLSVerify() bool {
return r.skipTLSVerify
}
func (r *Request) SetSkipTLSVerify(skipTLSVerify bool) *Request {
r.skipTLSVerify = skipTLSVerify
return r
}
func (r *Request) TlsConfig() *tls.Config {
return r.tlsConfig
}
func (r *Request) SetTlsConfig(tlsConfig *tls.Config) *Request {
r.tlsConfig = tlsConfig
return r
}
func (r *Request) AutoFetchRespBody() bool {
return r.autoFetchRespBody
}
func (r *Request) SetAutoFetchRespBody(autoFetchRespBody bool) *Request {
r.autoFetchRespBody = autoFetchRespBody
return r
}
func (r *Request) ResetReqHeader() *Request {
r.headers = make(http.Header)
return r
}
func (r *Request) ResetReqCookies() *Request {
r.cookies = []*http.Cookie{}
return r
}
func (r *Request) AddSimpleCookie(key, value string) *Request {
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: "/"})
return r
}
func (r *Request) AddCookie(key, value, path string) *Request {
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: path})
return r
}
func (r *Request) AddFile(formName, filepath string) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: f,
FileSize: stat.Size(),
FileType: "application/octet-stream",
})
return nil
}
func (r *Request) AddFileWithName(formName, filepath, filename string) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: f,
FileSize: stat.Size(),
FileType: "application/octet-stream",
})
return nil
}
func (r *Request) AddFileWithType(formName, filepath, filetype string) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: f,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
}
func (r *Request) AddFileWithNameAndType(formName, filepath, filename, filetype string) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: f,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
}
func (r *Request) AddFileNoError(formName, filepath string) *Request {
r.AddFile(formName, filepath)
return r
}
func (r *Request) AddFileWithNameNoError(formName, filepath, filename string) *Request {
r.AddFileWithName(formName, filepath, filename)
return r
}
func (r *Request) AddFileWithTypeNoError(formName, filepath, filetype string) *Request {
r.AddFileWithType(formName, filepath, filetype)
return r
}
func (r *Request) AddFileWithNameAndTypeNoError(formName, filepath, filename, filetype string) *Request {
r.AddFileWithNameAndType(formName, filepath, filename, filetype)
return r
}
type RequestFile struct {
FormName string
FileName string
FileData io.Reader
FileSize int64
FileType string
}
type RequestOpt func(opt *RequestOpts) error
2022-03-14 15:43:56 +08:00
2024-08-08 22:03:10 +08:00
// if doRawTransport is true, this function will nolonger work
2022-03-14 15:43:56 +08:00
func WithDialTimeout(timeout time.Duration) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.dialTimeout = timeout
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawTransport is true, this function will nolonger work
2022-03-14 15:43:56 +08:00
func WithTimeout(timeout time.Duration) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.timeout = timeout
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawTransport is true, this function will nolonger work
func WithTlsConfig(tlscfg *tls.Config) RequestOpt {
return func(opt *RequestOpts) error {
opt.tlsConfig = tlscfg
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawRequest is true, this function will nolonger work
func WithHeader(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.headers.Set(key, val)
return nil
2022-08-22 16:22:22 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawRequest is true, this function will nolonger work
2022-03-14 15:43:56 +08:00
func WithHeaderMap(header map[string]string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
2022-03-14 15:43:56 +08:00
for key, val := range header {
2024-08-08 22:03:10 +08:00
opt.headers.Set(key, val)
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawRequest is true, this function will nolonger work
func WithReader(r io.Reader) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyDataReader = r
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
// if doRawRequest is true, this function will nolonger work
func WithBytes(r []byte) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyDataBytes = r
return nil
}
}
// if doRawRequest is true, this function will nolonger work
func WithFormData(data map[string][]string) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFormData = data
return nil
}
}
// if doRawRequest is true, this function will nolonger work
func WithFileDatas(data []RequestFile) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFileData = data
return nil
}
}
// if doRawRequest is true, this function will nolonger work
func WithFileData(data RequestFile) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFileData = append(opt.bodyFileData, data)
return nil
}
}
// if doRawRequest is true, this function will nolonger work
func WithAddFile(formName, filepath string) RequestOpt {
return func(opt *RequestOpts) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: f,
FileSize: stat.Size(),
FileType: "application/octet-stream",
})
return nil
}
}
func WithAddFileWithName(formName, filepath, filename string) RequestOpt {
return func(opt *RequestOpts) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: f,
FileSize: stat.Size(),
FileType: "application/octet-stream",
})
return nil
}
}
func WithAddFileWithType(formName, filepath, filetype string) RequestOpt {
return func(opt *RequestOpts) error {
f, err := os.Open(filepath)
if err != nil {
return nil
}
stat, err := f.Stat()
if err != nil {
return nil
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: f,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
}
}
func WithAddFileWithNameAndType(formName, filepath, filename, filetype string) RequestOpt {
return func(opt *RequestOpts) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: f,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithFetchRespBody(fetch bool) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.autoFetchRespBody = fetch
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithCookies(ck []*http.Cookie) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.cookies = ck
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithCookie(key, val, path string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
return nil
}
}
func WithSimpleCookie(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: "/"})
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithCookieMap(header map[string]string, path string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
2022-03-14 15:43:56 +08:00
for key, val := range header {
2024-08-08 22:03:10 +08:00
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithQueries(queries map[string][]string) RequestOpt {
return func(opt *RequestOpts) error {
opt.queries = queries
return nil
}
}
func WithAddQueries(queries map[string]string) RequestOpt {
return func(opt *RequestOpts) error {
for k, v := range queries {
opt.queries[k] = append(opt.queries[k], v)
}
return nil
}
}
func WithAddQuery(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.queries[key] = append(opt.queries[key], val)
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithProxy(proxy string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.proxy = proxy
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithProcess(fn func(string, int64, int64)) RequestOpt {
return func(opt *RequestOpts) error {
opt.FileUploadRecallFn = fn
return nil
2022-03-14 15:43:56 +08:00
}
2020-07-20 11:17:29 +08:00
}
2022-03-14 15:43:56 +08:00
func WithContentType(ct string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.headers.Set("Content-Type", ct)
return nil
2022-03-14 15:43:56 +08:00
}
}
func WithUserAgent(ua string) RequestOpt {
2024-08-08 22:03:10 +08:00
return func(opt *RequestOpts) error {
opt.headers.Set("User-Agent", ua)
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithSkipTLSVerify(skip bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.skipTLSVerify = skip
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithDisableRedirect(disable bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.disableRedirect = disable
return nil
2022-03-14 15:43:56 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithDoRawRequest(doRawRequest bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.doRawRequest = doRawRequest
return nil
2022-06-06 11:18:42 +08:00
}
}
2024-08-08 22:03:10 +08:00
func WithDoRawClient(doRawClient bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.doRawClient = doRawClient
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithDoRawTransport(doRawTrans bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.doRawTransport = doRawTrans
return nil
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithTransport(hs *http.Transport) RequestOpt {
return func(opt *RequestOpts) error {
opt.transport = hs
return nil
2020-10-19 21:04:56 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithRawRequest(req *http.Request) RequestOpt {
return func(opt *RequestOpts) error {
opt.rawRequest = req
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithRawClient(hc *http.Client) RequestOpt {
return func(opt *RequestOpts) error {
opt.rawClient = hc
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithCustomHostIP(ip []string) RequestOpt {
return func(opt *RequestOpts) error {
if len(ip) == 0 {
return nil
}
for _, v := range ip {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom ip: %s", v)
}
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
opt.customIP = ip
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithAddCustomHostIP(ip string) RequestOpt {
return func(opt *RequestOpts) error {
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid custom ip: %s", ip)
}
opt.customIP = append(opt.customIP, ip)
return nil
2022-08-22 16:22:22 +08:00
}
2024-08-08 22:03:10 +08:00
}
func WithLookUpFn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) RequestOpt {
return func(opt *RequestOpts) error {
if lookUpIPfn == nil {
opt.alreadySetLookUpIPfn = false
opt.lookUpIPfn = net.DefaultResolver.LookupIPAddr
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
opt.lookUpIPfn = lookUpIPfn
opt.alreadySetLookUpIPfn = true
return nil
}
}
// WithCustomDNS will use custom dns to resolve the host
// Note: if LookUpIPfn is set, this function will not be used
func WithCustomDNS(customDNS []string) RequestOpt {
return func(opt *RequestOpts) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
opt.customDNS = customDNS
return nil
2022-03-14 15:43:56 +08:00
}
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
// WithAddCustomDNS will use a custom dns to resolve the host
// Note: if LookUpIPfn is set, this function will not be used
func WithAddCustomDNS(customDNS string) RequestOpt {
return func(opt *RequestOpts) error {
if net.ParseIP(customDNS) == nil {
return fmt.Errorf("invalid custom dns: %s", customDNS)
}
opt.customDNS = append(opt.customDNS, customDNS)
return nil
}
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
// WithAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
// also the memory usage will be higher
// Note that this function will not work if doRawRequest is true
func (r *RequestOpts) WithAutoCalcContentLength(autoCalcContentLength bool) RequestOpt {
return func(opt *RequestOpts) error {
r.autoCalcContentLength = autoCalcContentLength
return nil
}
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
type Response struct {
*http.Response
req Request
data *Body
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
type Body struct {
full []byte
raw io.ReadCloser
isFull bool
2025-04-28 13:19:45 +08:00
sync.Mutex
2022-03-14 15:43:56 +08:00
}
2020-07-20 11:17:29 +08:00
2024-08-08 22:03:10 +08:00
func (b *Body) readAll() {
2025-04-28 13:19:45 +08:00
b.Lock()
defer b.Unlock()
2024-08-08 22:03:10 +08:00
if !b.isFull {
2025-04-28 13:19:45 +08:00
if b.raw == nil {
b.isFull = true
return
}
2024-08-08 22:03:10 +08:00
b.full, _ = io.ReadAll(b.raw)
b.isFull = true
b.raw.Close()
2020-07-20 11:17:29 +08:00
}
}
2024-08-08 22:03:10 +08:00
func (b *Body) String() string {
b.readAll()
return string(b.full)
}
func (b *Body) Bytes() []byte {
b.readAll()
return b.full
}
func (b *Body) Unmarshal(u interface{}) error {
b.readAll()
return json.Unmarshal(b.full, u)
}
// Reader returns a reader for the body
// if this function is called, other functions like String, Bytes, Unmarshal may not work
func (b *Body) Reader() io.ReadCloser {
2025-04-28 13:19:45 +08:00
b.Lock()
defer b.Unlock()
2024-08-08 22:03:10 +08:00
if b.isFull {
return io.NopCloser(bytes.NewReader(b.full))
2020-07-21 09:20:02 +08:00
}
2024-08-08 22:03:10 +08:00
b.isFull = true
return b.raw
}
func (b *Body) Close() error {
2024-08-30 23:44:49 +08:00
return b.raw.Close()
2024-08-08 22:03:10 +08:00
}
func (r *Response) GetRequest() Request {
return r.req
}
func (r *Response) Body() *Body {
return r.data
}
func Curl(r *Request) (*Response, error) {
r.errInfo = nil
err := applyOptions(r)
2020-07-21 09:20:02 +08:00
if err != nil {
2024-08-08 22:03:10 +08:00
return nil, fmt.Errorf("apply options error: %s", err)
2020-07-21 09:20:02 +08:00
}
2024-08-08 22:03:10 +08:00
resp, err := r.rawClient.Do(r.rawRequest)
var res = Response{
Response: resp,
req: *r,
data: new(Body),
2020-07-21 09:20:02 +08:00
}
2024-08-08 22:03:10 +08:00
if err != nil {
res.Response = &http.Response{}
return &res, fmt.Errorf("do request error: %s", err)
}
res.data.raw = resp.Body
if r.autoFetchRespBody {
res.data.full, _ = io.ReadAll(resp.Body)
res.data.isFull = true
resp.Body.Close()
2020-07-21 09:20:02 +08:00
}
2024-08-08 22:03:10 +08:00
return &res, r.errInfo
}
func NewReq(uri string, opts ...RequestOpt) *Request {
return NewSimpleRequest(uri, "GET", opts...)
}
func NewReqWithContext(ctx context.Context, uri string, opts ...RequestOpt) *Request {
return NewSimpleRequestWithContext(ctx, uri, "GET", opts...)
2020-07-21 09:20:02 +08:00
}
2024-08-08 22:03:10 +08:00
func NewSimpleRequest(uri string, method string, opts ...RequestOpt) *Request {
r, _ := newRequest(context.Background(), uri, method, opts...)
return r
2022-03-11 09:29:09 +08:00
}
2024-08-08 22:03:10 +08:00
func NewRequest(uri string, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(context.Background(), uri, method, opts...)
}
func NewSimpleRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) *Request {
r, _ := newRequest(ctx, uri, method, opts...)
return r
}
func NewRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(ctx, uri, method, opts...)
}
func newRequest(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
2020-07-20 11:17:29 +08:00
var req *http.Request
2020-07-21 09:20:02 +08:00
var err error
2024-08-08 22:03:10 +08:00
if method == "" {
method = "GET"
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
method = strings.ToUpper(method)
req, err = http.NewRequestWithContext(ctx, method, uri, nil)
2020-07-20 11:17:29 +08:00
if err != nil {
2024-08-08 22:03:10 +08:00
return nil, err
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
var r = &Request{
ctx: ctx,
uri: uri,
method: method,
RequestOpts: RequestOpts{
rawRequest: req,
rawClient: new(http.Client),
timeout: DefaultTimeout,
dialTimeout: DefaultDialTimeout,
autoFetchRespBody: DefaultFetchRespBody,
lookUpIPfn: net.DefaultResolver.LookupIPAddr,
bodyFormData: make(map[string][]string),
queries: make(map[string][]string),
},
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
r.headers = make(http.Header)
if strings.ToUpper(method) == "POST" {
r.headers.Set("Content-Type", HEADER_FORM_URLENCODE)
}
r.headers.Set("User-Agent", "B612 / 1.2.0")
for _, v := range opts {
if v != nil {
err = v(&r.RequestOpts)
if err != nil {
return nil, err
}
2020-10-19 21:04:56 +08:00
}
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
if r.transport == nil {
r.transport = &http.Transport{}
2020-07-20 11:17:29 +08:00
}
2024-08-08 22:03:10 +08:00
if r.doRawTransport {
if r.skipTLSVerify {
if r.transport.TLSClientConfig == nil {
r.transport.TLSClientConfig = &tls.Config{}
}
r.transport.TLSClientConfig.InsecureSkipVerify = true
}
if r.tlsConfig != nil {
r.transport.TLSClientConfig = r.tlsConfig
}
r.transport.DialContext = func(ctx context.Context, netType, addr string) (net.Conn, error) {
var lastErr error
var addrs []string
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if len(r.customIP) > 0 {
for _, v := range r.customIP {
ipAddr := net.ParseIP(v)
if ipAddr == nil {
return nil, fmt.Errorf("invalid custom ip: %s", r.customIP)
}
tmpAddr := net.JoinHostPort(v, port)
addrs = append(addrs, tmpAddr)
}
} else {
ipLists, err := r.lookUpIPfn(ctx, host)
if err != nil {
return nil, err
}
for _, v := range ipLists {
tmpAddr := net.JoinHostPort(v.String(), port)
addrs = append(addrs, tmpAddr)
}
}
for _, addr := range addrs {
c, err := net.DialTimeout(netType, addr, r.dialTimeout)
if err != nil {
lastErr = err
continue
}
if r.timeout != 0 {
err = c.SetDeadline(time.Now().Add(r.timeout))
}
return c, nil
}
return nil, lastErr
2022-06-06 11:18:42 +08:00
}
}
2024-08-08 22:03:10 +08:00
return r, nil
2020-07-20 11:17:29 +08:00
}
2020-10-19 21:04:56 +08:00
2024-08-08 22:03:10 +08:00
func applyDataReader(r *Request) error {
// 优先度为bodyDataReader > bodyDataBytes > bodyFormData > bodyFileData
if r.bodyDataReader != nil {
r.rawRequest.Body = io.NopCloser(r.bodyDataReader)
return nil
}
if len(r.bodyDataBytes) != 0 {
r.rawRequest.Body = io.NopCloser(bytes.NewReader(r.bodyDataBytes))
return nil
}
if len(r.bodyFormData) != 0 && len(r.bodyFileData) == 0 {
var body = url.Values{}
for k, v := range r.bodyFormData {
for _, vv := range v {
body.Add(k, vv)
}
}
r.rawRequest.Body = io.NopCloser(strings.NewReader(body.Encode()))
return nil
}
if len(r.bodyFileData) != 0 {
var pr, pw = io.Pipe()
var w = multipart.NewWriter(pw)
r.rawRequest.Header.Set("Content-Type", w.FormDataContentType())
go func() {
defer pw.Close() // ensure pipe writer is closed
2020-10-19 21:04:56 +08:00
2024-08-08 22:03:10 +08:00
if len(r.bodyFormData) != 0 {
for k, v := range r.bodyFormData {
for _, vv := range v {
if err := w.WriteField(k, vv); err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
}
}
}
for _, v := range r.bodyFileData {
var fw, err = w.CreateFormFile(v.FormName, v.FileName)
if err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
if _, err := copyWithContext(r.ctx, r.FileUploadRecallFn, v.FileName, v.FileSize, fw, v.FileData); err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
}
2020-10-19 21:04:56 +08:00
2024-08-08 22:03:10 +08:00
if err := w.Close(); err != nil {
pw.CloseWithError(err) // close pipe with error if writer close fails
}
}()
2020-10-19 21:04:56 +08:00
2024-08-08 22:03:10 +08:00
r.rawRequest.Body = pr
return nil
2020-10-19 21:04:56 +08:00
}
2024-08-08 22:03:10 +08:00
return nil
2020-10-19 21:04:56 +08:00
}
2022-03-14 15:43:56 +08:00
2024-08-08 22:03:10 +08:00
func applyOptions(r *Request) error {
defer func() {
r.alreadyApply = true
}()
var req = r.rawRequest
if !r.doRawRequest {
if r.queries != nil {
sid := req.URL.Query()
for k, v := range r.queries {
for _, vv := range v {
sid.Add(k, vv)
}
}
req.URL.RawQuery = sid.Encode()
}
for k, v := range r.headers {
for _, vv := range v {
req.Header.Add(k, vv)
}
}
if len(r.cookies) != 0 {
for _, v := range r.cookies {
req.AddCookie(v)
}
}
if r.basicAuth[0] != "" || r.basicAuth[1] != "" {
req.SetBasicAuth(r.basicAuth[0], r.basicAuth[1])
}
err := applyDataReader(r)
if err != nil {
return fmt.Errorf("apply data reader error: %s", err)
}
if r.autoCalcContentLength {
if req.Body != nil {
data, err := io.ReadAll(req.Body)
if err != nil {
return fmt.Errorf("read data error: %s", err)
}
req.Header.Set("Content-Length", strconv.Itoa(len(data)))
req.Body = io.NopCloser(bytes.NewReader(data))
}
}
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
if r.proxy != "" {
purl, err := url.Parse(r.proxy)
if err != nil {
return fmt.Errorf("parse proxy url error: %s", err)
}
r.transport.Proxy = http.ProxyURL(purl)
}
if !r.doRawClient {
if !r.doRawTransport {
if !r.alreadySetLookUpIPfn && len(r.customIP) > 0 {
resolver := net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
for _, addr := range r.customIP {
if conn, err = net.Dial("udp", addr+":53"); err != nil {
continue
} else {
return conn, nil
}
}
return
},
}
r.lookUpIPfn = resolver.LookupIPAddr
}
}
r.rawClient.Transport = r.transport
if r.disableRedirect {
r.rawClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
}
return nil
2022-03-14 15:43:56 +08:00
}
2024-08-08 22:03:10 +08:00
func copyWithContext(ctx context.Context, recall func(string, int64, int64), filename string, total int64, dst io.Writer, src io.Reader) (written int64, err error) {
pr, pw := io.Pipe()
defer pr.Close()
2024-08-08 22:03:10 +08:00
go func() {
defer pw.Close()
_, err := io.Copy(pw, src)
if err != nil {
pw.CloseWithError(err)
}
}()
var count int64
buf := make([]byte, 4096)
for {
select {
case <-ctx.Done():
return written, ctx.Err()
default:
nr, err := pr.Read(buf)
if err != nil {
if err == io.EOF {
go recall(filename, count, total)
return written, nil
}
return written, err
}
count += int64(nr)
if recall != nil {
go recall(filename, count, total)
}
nw, err := dst.Write(buf[:nr])
if err != nil {
return written, err
}
if nr != nw {
return written, io.ErrShortWrite
}
written += int64(nr)
}
}
}