1 Star 0 Fork 0

jacky / req

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
req.go 14.94 KB
一键复制 编辑 原始数据 按行查看 历史
jacky 提交于 2021-04-27 11:45 . 修改成功
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
package req
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// default *Req
var std = New()
// flags to decide which part can be outputed
const (
LreqHead = 1 << iota // output request head (request line and request header)
LreqBody // output request body
LrespHead // output response head (response line and response header)
LrespBody // output response body
Lcost // output time costed by the request
LstdFlags = LreqHead | LreqBody | LrespHead | LrespBody
)
// Param represents http request param
type Param map[string]interface{}
// QueryParam is used to force append http request param to the uri
type QueryParam map[string]interface{}
// Host is used for set request's Host
type Host string
// FileUpload represents a file to upload
type FileUpload struct {
// filename in multipart form.
FileName string
// form field name
FieldName string
// file to uplaod, required
File io.ReadCloser
}
type DownloadProgress func(current, total int64)
type UploadProgress func(current, total int64)
// File upload files matching the name pattern such as
// /usr/*/bin/go* (assuming the Separator is '/')
func File(patterns ...string) interface{} {
matches := []string{}
for _, pattern := range patterns {
m, err := filepath.Glob(pattern)
if err != nil {
return err
}
matches = append(matches, m...)
}
if len(matches) == 0 {
return errors.New("req: no file have been matched")
}
uploads := []FileUpload{}
for _, match := range matches {
if s, e := os.Stat(match); e != nil || s.IsDir() {
continue
}
file, _ := os.Open(match)
uploads = append(uploads, FileUpload{
File: file,
FileName: filepath.Base(match),
FieldName: "media",
})
}
return uploads
}
type bodyJson struct {
v interface{}
}
type bodyXml struct {
v interface{}
}
// BodyJSON make the object be encoded in json format and set it to the request body
func BodyJSON(v interface{}) *bodyJson {
return &bodyJson{v: v}
}
// BodyXML make the object be encoded in xml format and set it to the request body
func BodyXML(v interface{}) *bodyXml {
return &bodyXml{v: v}
}
// Req is a convenient client for initiating requests
type Req struct {
client *http.Client
jsonEncOpts *jsonEncOpts
xmlEncOpts *xmlEncOpts
flag int
progressInterval time.Duration
}
// New create a new *Req
func New() *Req {
// default progress reporting interval is 200 milliseconds
return &Req{flag: LstdFlags, progressInterval: 200 * time.Millisecond}
}
type param struct {
url.Values
}
func (p *param) getValues() url.Values {
if p.Values == nil {
p.Values = make(url.Values)
}
return p.Values
}
func (p *param) Copy(pp param) {
if pp.Values == nil {
return
}
vs := p.getValues()
for key, values := range pp.Values {
for _, value := range values {
vs.Add(key, value)
}
}
}
func (p *param) Adds(m map[string]interface{}) {
if len(m) == 0 {
return
}
vs := p.getValues()
for k, v := range m {
vs.Add(k, fmt.Sprint(v))
}
}
func (p *param) Empty() bool {
return p.Values == nil
}
// Do execute a http request with sepecify method and url,
// and it can also have some optional params, depending on your needs.
func (r *Req) Do(method, rawurl string, vs ...interface{}) (resp *Resp, err error) {
if rawurl == "" {
return nil, errors.New("req: url not specified")
}
req := &http.Request{
Method: method,
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
}
resp = &Resp{req: req, r: r}
var queryParam param
var formParam param
var uploads []FileUpload
var uploadProgress UploadProgress
var progress func(int64, int64)
var delayedFunc []func()
var lastFunc []func()
for _, v := range vs {
switch vv := v.(type) {
case Header:
for key, value := range vv {
req.Header.Add(key, value)
}
case http.Header:
for key, values := range vv {
//for _, value := range values {
// req.Header.Add(key, value)
//}
req.Header[key] = values
}
case *bodyJson:
fn, err := setBodyJson(req, resp, r.jsonEncOpts, vv.v)
if err != nil {
return nil, err
}
delayedFunc = append(delayedFunc, fn)
case *bodyXml:
fn, err := setBodyXml(req, resp, r.xmlEncOpts, vv.v)
if err != nil {
return nil, err
}
delayedFunc = append(delayedFunc, fn)
case url.Values:
p := param{vv}
if method == "GET" || method == "HEAD" {
queryParam.Copy(p)
} else {
formParam.Copy(p)
}
case Param:
if method == "GET" || method == "HEAD" {
queryParam.Adds(vv)
} else {
formParam.Adds(vv)
}
case QueryParam:
queryParam.Adds(vv)
case string:
setBodyBytes(req, resp, []byte(vv))
case []byte:
setBodyBytes(req, resp, vv)
case bytes.Buffer:
setBodyBytes(req, resp, vv.Bytes())
case *http.Client:
resp.client = vv
case FileUpload:
uploads = append(uploads, vv)
case []FileUpload:
uploads = append(uploads, vv...)
case *http.Cookie:
req.AddCookie(vv)
case Host:
req.Host = string(vv)
case io.Reader:
fn := setBodyReader(req, resp, vv)
lastFunc = append(lastFunc, fn)
case UploadProgress:
uploadProgress = vv
case DownloadProgress:
resp.downloadProgress = vv
case func(int64, int64):
progress = vv
case context.Context:
req = req.WithContext(vv)
resp.req = req
case error:
return nil, vv
}
}
if length := req.Header.Get("Content-Length"); length != "" {
if l, err := strconv.ParseInt(length, 10, 64); err == nil {
req.ContentLength = l
}
}
if len(uploads) > 0 && (req.Method == "POST" || req.Method == "PUT") { // multipart
var up UploadProgress
if uploadProgress != nil {
up = uploadProgress
} else if progress != nil {
up = UploadProgress(progress)
}
multipartHelper := &multipartHelper{
form: formParam.Values,
uploads: uploads,
uploadProgress: up,
progressInterval: resp.r.progressInterval,
}
multipartHelper.Upload(req)
resp.multipartHelper = multipartHelper
} else {
if progress != nil {
resp.downloadProgress = DownloadProgress(progress)
}
if !formParam.Empty() {
if req.Body != nil {
queryParam.Copy(formParam)
} else {
setBodyBytes(req, resp, []byte(formParam.Encode()))
setContentType(req, "application/x-www-form-urlencoded; charset=UTF-8")
}
}
}
if !queryParam.Empty() {
paramStr := queryParam.Encode()
if strings.IndexByte(rawurl, '?') == -1 {
rawurl = rawurl + "?" + paramStr
} else {
rawurl = rawurl + "&" + paramStr
}
}
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
req.URL = u
if host := req.Header.Get("Host"); host != "" {
req.Host = host
}
for _, fn := range delayedFunc {
fn()
}
if resp.client == nil {
resp.client = r.Client()
}
var response *http.Response
if r.flag&Lcost != 0 {
before := time.Now()
response, err = resp.client.Do(req)
after := time.Now()
resp.cost = after.Sub(before)
} else {
response, err = resp.client.Do(req)
}
if err != nil {
return nil, err
}
for _, fn := range lastFunc {
fn()
}
resp.resp = response
if _, ok := resp.client.Transport.(*http.Transport); ok && response.Header.Get("Content-Encoding") == "gzip" && req.Header.Get("Accept-Encoding") != "" {
body, err := gzip.NewReader(response.Body)
if err != nil {
return nil, err
}
response.Body = body
}
// output detail if Debug is enabled
if Debug {
fmt.Println(resp.Dump())
}
return
}
func setBodyBytes(req *http.Request, resp *Resp, data []byte) {
resp.reqBody = data
req.Body = ioutil.NopCloser(bytes.NewReader(data))
req.ContentLength = int64(len(data))
}
func setBodyJson(req *http.Request, resp *Resp, opts *jsonEncOpts, v interface{}) (func(), error) {
var data []byte
switch vv := v.(type) {
case string:
data = []byte(vv)
case []byte:
data = vv
case *bytes.Buffer:
data = vv.Bytes()
default:
if opts != nil {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent(opts.indentPrefix, opts.indentValue)
enc.SetEscapeHTML(opts.escapeHTML)
err := enc.Encode(v)
if err != nil {
return nil, err
}
data = buf.Bytes()
} else {
var err error
data, err = json.Marshal(v)
if err != nil {
return nil, err
}
}
}
setBodyBytes(req, resp, data)
delayedFunc := func() {
setContentType(req, "application/json; charset=UTF-8")
}
return delayedFunc, nil
}
func setBodyXml(req *http.Request, resp *Resp, opts *xmlEncOpts, v interface{}) (func(), error) {
var data []byte
switch vv := v.(type) {
case string:
data = []byte(vv)
case []byte:
data = vv
case *bytes.Buffer:
data = vv.Bytes()
default:
if opts != nil {
var buf bytes.Buffer
enc := xml.NewEncoder(&buf)
enc.Indent(opts.prefix, opts.indent)
err := enc.Encode(v)
if err != nil {
return nil, err
}
data = buf.Bytes()
} else {
var err error
data, err = xml.Marshal(v)
if err != nil {
return nil, err
}
}
}
setBodyBytes(req, resp, data)
delayedFunc := func() {
setContentType(req, "application/xml; charset=UTF-8")
}
return delayedFunc, nil
}
func setContentType(req *http.Request, contentType string) {
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", contentType)
}
}
func setBodyReader(req *http.Request, resp *Resp, rd io.Reader) func() {
var rc io.ReadCloser
switch r := rd.(type) {
case *os.File:
stat, err := r.Stat()
if err == nil {
req.ContentLength = stat.Size()
}
rc = r
case io.ReadCloser:
rc = r
default:
rc = ioutil.NopCloser(rd)
}
bw := &bodyWrapper{
ReadCloser: rc,
limit: 102400,
}
req.Body = bw
lastFunc := func() {
resp.reqBody = bw.buf.Bytes()
}
return lastFunc
}
type bodyWrapper struct {
io.ReadCloser
buf bytes.Buffer
limit int
}
func (b *bodyWrapper) Read(p []byte) (n int, err error) {
n, err = b.ReadCloser.Read(p)
if left := b.limit - b.buf.Len(); left > 0 && n > 0 {
if n <= left {
b.buf.Write(p[:n])
} else {
b.buf.Write(p[:left])
}
}
return
}
type multipartHelper struct {
form url.Values
uploads []FileUpload
dump []byte
uploadProgress UploadProgress
progressInterval time.Duration
}
func (m *multipartHelper) Upload(req *http.Request) {
pr, pw := io.Pipe()
bodyWriter := multipart.NewWriter(pw)
go func() {
for key, values := range m.form {
for _, value := range values {
bodyWriter.WriteField(key, value)
}
}
var upload func(io.Writer, io.Reader) error
if m.uploadProgress != nil {
var total int64
for _, up := range m.uploads {
if file, ok := up.File.(*os.File); ok {
stat, err := file.Stat()
if err != nil {
continue
}
total += stat.Size()
}
}
var current int64
buf := make([]byte, 1024)
var lastTime time.Time
defer func() {
m.uploadProgress(current, total)
}()
upload = func(w io.Writer, r io.Reader) error {
for {
n, err := r.Read(buf)
if n > 0 {
_, _err := w.Write(buf[:n])
if _err != nil {
return _err
}
current += int64(n)
if now := time.Now(); now.Sub(lastTime) > m.progressInterval {
lastTime = now
m.uploadProgress(current, total)
}
}
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}
}
i := 0
for _, up := range m.uploads {
if up.FieldName == "" {
i++
up.FieldName = "file" + strconv.Itoa(i)
}
fileWriter, err := bodyWriter.CreateFormFile(up.FieldName, up.FileName)
if err != nil {
continue
}
//iocopy
if upload == nil {
io.Copy(fileWriter, up.File)
} else {
if _, ok := up.File.(*os.File); ok {
upload(fileWriter, up.File)
} else {
io.Copy(fileWriter, up.File)
}
}
up.File.Close()
}
bodyWriter.Close()
pw.Close()
}()
req.Header.Set("Content-Type", bodyWriter.FormDataContentType())
req.Body = ioutil.NopCloser(pr)
}
func (m *multipartHelper) Dump() []byte {
if m.dump != nil {
return m.dump
}
var buf bytes.Buffer
bodyWriter := multipart.NewWriter(&buf)
for key, values := range m.form {
for _, value := range values {
m.writeField(bodyWriter, key, value)
}
}
for _, up := range m.uploads {
m.writeFile(bodyWriter, up.FieldName, up.FileName)
}
bodyWriter.Close()
m.dump = buf.Bytes()
return m.dump
}
func (m *multipartHelper) writeField(w *multipart.Writer, fieldname, value string) error {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"`, fieldname))
p, err := w.CreatePart(h)
if err != nil {
return err
}
_, err = p.Write([]byte(value))
return err
}
func (m *multipartHelper) writeFile(w *multipart.Writer, fieldname, filename string) error {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
fieldname, filename))
h.Set("Content-Type", "application/octet-stream")
p, err := w.CreatePart(h)
if err != nil {
return err
}
_, err = p.Write([]byte("******"))
return err
}
// Get execute a http GET request
func (r *Req) Get(url string, v ...interface{}) (*Resp, error) {
return r.Do("GET", url, v...)
}
// Post execute a http POST request
func (r *Req) Post(url string, v ...interface{}) (*Resp, error) {
return r.Do("POST", url, v...)
}
// Put execute a http PUT request
func (r *Req) Put(url string, v ...interface{}) (*Resp, error) {
return r.Do("PUT", url, v...)
}
// Patch execute a http PATCH request
func (r *Req) Patch(url string, v ...interface{}) (*Resp, error) {
return r.Do("PATCH", url, v...)
}
// Delete execute a http DELETE request
func (r *Req) Delete(url string, v ...interface{}) (*Resp, error) {
return r.Do("DELETE", url, v...)
}
// Head execute a http HEAD request
func (r *Req) Head(url string, v ...interface{}) (*Resp, error) {
return r.Do("HEAD", url, v...)
}
// Options execute a http OPTIONS request
func (r *Req) Options(url string, v ...interface{}) (*Resp, error) {
return r.Do("OPTIONS", url, v...)
}
// Get execute a http GET request
func Get(url string, v ...interface{}) (*Resp, error) {
return std.Get(url, v...)
}
// Post execute a http POST request
func Post(url string, v ...interface{}) (*Resp, error) {
return std.Post(url, v...)
}
// Put execute a http PUT request
func Put(url string, v ...interface{}) (*Resp, error) {
return std.Put(url, v...)
}
// Head execute a http HEAD request
func Head(url string, v ...interface{}) (*Resp, error) {
return std.Head(url, v...)
}
// Options execute a http OPTIONS request
func Options(url string, v ...interface{}) (*Resp, error) {
return std.Options(url, v...)
}
// Delete execute a http DELETE request
func Delete(url string, v ...interface{}) (*Resp, error) {
return std.Delete(url, v...)
}
// Patch execute a http PATCH request
func Patch(url string, v ...interface{}) (*Resp, error) {
return std.Patch(url, v...)
}
// Do execute request.
func Do(method, url string, v ...interface{}) (*Resp, error) {
return std.Do(method, url, v...)
}
1
https://gitee.com/jacky-xyy/req.git
git@gitee.com:jacky-xyy/req.git
jacky-xyy
req
req
10b1ce3b477e

搜索帮助