Skip to content

Commit 2d22ff0

Browse files
committed
support upload callback(#104)
1 parent 780943c commit 2d22ff0

File tree

15 files changed

+374
-31
lines changed

15 files changed

+374
-31
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,29 @@ client.R().SetFileReader("avatar", "avatar.png", avatarImgFile).Post(url)
781781
*/
782782
```
783783

784+
**Upload Callback**
785+
786+
You can set `UploadCallback` if you want to show upload progress:
787+
788+
```go
789+
client := req.C()
790+
client.R().
791+
SetFile("excel", "test.xlsx").
792+
SetUploadCallback(func(info req.UploadInfo) {
793+
fmt.Printf("%q uploaded %.2f%%\n", info.FileName, float64(info.UploadedSize)/float64(info.FileSize)*100.0)
794+
}).Post("https://exmaple.com/upload")
795+
/* Output
796+
"test.xlsx" uploaded 7.44%
797+
"test.xlsx" uploaded 29.78%
798+
"test.xlsx" uploaded 52.08%
799+
"test.xlsx" uploaded 74.47%
800+
"test.xlsx" uploaded 96.87%
801+
"test.xlsx" uploaded 100.00%
802+
*/
803+
```
804+
805+
> `UploadCallback` will be invoked at least every 200ms by default, you can customize the minimal invoke interval using `SetUploadCallbackWithInterval`.
806+
784807
## <a name="AutoDecode">Auto-Decode</a>
785808

786809
`Req` detect the charset of response body and decode it to utf-8 automatically to avoid garbled characters by default.

docs/api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ Basically, you can know the meaning of most settings directly from the method na
203203
* [SetFileBytes(paramName, filename string, content []byte)](https://pkg.go.dev/github.com/imroc/req/v3#Request.SetFileBytes)
204204
* [SetFileReader(paramName, filePath string, reader io.Reader)](https://pkg.go.dev/github.com/imroc/req/v3#Request.SetFileReader)
205205
* [SetFileUpload(uploads ...FileUpload)](https://pkg.go.dev/github.com/imroc/req/v3#Request.SetFileUpload) - Set the fully custimized multipart file upload options.
206+
* [SetUploadCallback(callback UploadCallback)](https://pkg.go.dev/github.com/imroc/req/v3#Request.SetUploadCallback)
207+
* [SetUploadCallbackWithInterval(callback UploadCallback, minInterval time.Duration)](https://pkg.go.dev/github.com/imroc/req/v3#Request.SetUploadCallbackWithInterval)
206208

207209
### <a name="Download">Download</a>
208210

examples/uploadcallback/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# uploadcallback
2+
3+
This is a upload callback exmaple for `req`
4+
5+
## How to Run
6+
7+
Run `uploadserver`:
8+
9+
```go
10+
cd uploadserver
11+
go run .
12+
```
13+
14+
Run `uploadclient`:
15+
16+
```go
17+
cd uploadclient
18+
go run .
19+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module uploadclient
2+
3+
go 1.13
4+
5+
replace github.com/imroc/req/v3 => ../../../
6+
7+
require github.com/imroc/req/v3 v3.0.0
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
2+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
3+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
4+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
5+
golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s=
6+
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
7+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
8+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
9+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
10+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
11+
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
12+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
13+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"time"
7+
"github.com/imroc/req/v3"
8+
)
9+
10+
type SlowReader struct {
11+
Size int
12+
n int
13+
}
14+
15+
func (r *SlowReader) Close() error {
16+
return nil
17+
}
18+
19+
func (r *SlowReader) Read(p []byte) (int, error) {
20+
if r.n >= r.Size {
21+
return 0, io.EOF
22+
}
23+
time.Sleep(1 * time.Millisecond)
24+
n := len(p)
25+
if r.n+n >= r.Size {
26+
n = r.Size - r.n
27+
}
28+
for i := 0; i < n; i++ {
29+
p[i] = 'h'
30+
}
31+
r.n += n
32+
return n, nil
33+
}
34+
35+
func main() {
36+
size := 10 * 1024 * 1024
37+
req.SetFileUpload(req.FileUpload{
38+
ParamName: "file",
39+
FileName: "test.txt",
40+
GetFileContent: func() (io.ReadCloser, error) {
41+
return &SlowReader{Size: size}, nil
42+
},
43+
FileSize: int64(size),
44+
}).SetUploadCallbackWithInterval(func(info req.UploadInfo) {
45+
fmt.Printf("%s: %.2f%%\n", info.FileName, float64(info.UploadedSize)/float64(info.FileSize)*100.0)
46+
}, 1*time.Second).Post("http://127.0.0.1:8888/upload")
47+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module uploadserver
2+
3+
go 1.13
4+
5+
require github.com/gin-gonic/gin v1.7.7
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
5+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
6+
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
7+
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
8+
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
9+
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
10+
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
11+
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
12+
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
13+
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
14+
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
15+
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
16+
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
17+
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
18+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
19+
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
20+
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
21+
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
22+
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
23+
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
24+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
25+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
26+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
27+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
28+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
29+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
32+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
33+
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
34+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
35+
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
36+
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
37+
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
38+
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
39+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
40+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
41+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
42+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
43+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
44+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
46+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
47+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
48+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
49+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
50+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
51+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
52+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
53+
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
54+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
"io"
6+
"io/ioutil"
7+
"net/http"
8+
)
9+
10+
func main() {
11+
router := gin.Default()
12+
router.POST("/upload", func(c *gin.Context) {
13+
body := c.Request.Body
14+
io.Copy(ioutil.Discard, body)
15+
c.String(http.StatusOK, "ok")
16+
})
17+
router.Run(":8888")
18+
}

middleware.go

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package req
22

33
import (
44
"bytes"
5+
"errors"
56
"github.com/imroc/req/v3/internal/util"
67
"io"
78
"io/ioutil"
@@ -12,6 +13,7 @@ import (
1213
"os"
1314
"path/filepath"
1415
"strings"
16+
"time"
1517
)
1618

1719
type (
@@ -55,30 +57,90 @@ func closeq(v interface{}) {
5557
}
5658
}
5759

58-
func writeMultipartFormFile(w *multipart.Writer, file *FileUpload) error {
60+
func writeMultipartFormFile(w *multipart.Writer, file *FileUpload, r *Request) error {
5961
content, err := file.GetFileContent()
6062
if err != nil {
6163
return err
6264
}
6365
defer content.Close()
6466
// Auto detect actual multipart content type
6567
cbuf := make([]byte, 512)
68+
seeEOF := false
69+
lastTime := time.Now()
6670
size, err := content.Read(cbuf)
67-
if err != nil && err != io.EOF {
68-
return err
71+
if err != nil {
72+
if err == io.EOF {
73+
seeEOF = true
74+
} else {
75+
return err
76+
}
6977
}
7078

7179
pw, err := w.CreatePart(createMultipartHeader(file, http.DetectContentType(cbuf)))
7280
if err != nil {
7381
return err
7482
}
75-
7683
if _, err = pw.Write(cbuf[:size]); err != nil {
7784
return err
7885
}
86+
if seeEOF {
87+
return nil
88+
}
89+
if r.uploadCallback == nil {
90+
_, err = io.Copy(pw, content)
91+
return err
92+
}
7993

80-
_, err = io.Copy(pw, content)
81-
return err
94+
uploadedBytes := int64(size)
95+
progressCallback := func() {
96+
r.uploadCallback(UploadInfo{
97+
ParamName: file.ParamName,
98+
FileName: file.FileName,
99+
FileSize: file.FileSize,
100+
UploadedSize: uploadedBytes,
101+
})
102+
}
103+
if now := time.Now(); now.Sub(lastTime) >= r.uploadCallbackInterval {
104+
lastTime = now
105+
progressCallback()
106+
}
107+
buf := make([]byte, 1024)
108+
for {
109+
callback := false
110+
nr, er := content.Read(buf)
111+
if nr > 0 {
112+
nw, ew := pw.Write(buf[:nr])
113+
if nw < 0 || nr < nw {
114+
nw = 0
115+
if ew == nil {
116+
ew = errors.New("invalid write result")
117+
}
118+
}
119+
uploadedBytes += int64(nw)
120+
if ew != nil {
121+
return ew
122+
}
123+
if nr != nw {
124+
return io.ErrShortWrite
125+
}
126+
if now := time.Now(); now.Sub(lastTime) >= r.uploadCallbackInterval {
127+
lastTime = now
128+
progressCallback()
129+
callback = true
130+
}
131+
}
132+
if er != nil {
133+
if er == io.EOF {
134+
if !callback {
135+
progressCallback()
136+
}
137+
break
138+
} else {
139+
return er
140+
}
141+
}
142+
}
143+
return nil
82144
}
83145

84146
func writeMultiPart(r *Request, w *multipart.Writer, pw *io.PipeWriter) {
@@ -88,7 +150,7 @@ func writeMultiPart(r *Request, w *multipart.Writer, pw *io.PipeWriter) {
88150
}
89151
}
90152
for _, file := range r.uploadFiles {
91-
writeMultipartFormFile(w, file)
153+
writeMultipartFormFile(w, file, r)
92154
}
93155
w.Close() // close multipart to write tailer boundary
94156
pw.Close() // close pipe writer so that pipe reader could get EOF, and stop upload

req.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,33 @@ type FileUpload struct {
5353
FileName string
5454
// The file to be uploaded.
5555
GetFileContent GetContentFunc
56+
// Optional file length in bytes.
57+
FileSize int64
5658

59+
// Optional extra ContentDisposition parameters.
5760
// According to the HTTP specification, this should be nil,
5861
// but some servers may not follow the specification and
5962
// requires `Content-Disposition` parameters more than just
6063
// "name" and "filename".
6164
ExtraContentDisposition *ContentDisposition
6265
}
6366

67+
// UploadInfo is the information for each UploadCallback call.
68+
type UploadInfo struct {
69+
// parameter name in multipart upload
70+
ParamName string
71+
// filename in multipart upload
72+
FileName string
73+
// total file length in bytes.
74+
FileSize int64
75+
// uploaded file length in bytes.
76+
UploadedSize int64
77+
}
78+
79+
// UploadCallback is the callback which will be invoked during
80+
// multipart upload.
81+
type UploadCallback func(info UploadInfo)
82+
6483
func cloneCookies(cookies []*http.Cookie) []*http.Cookie {
6584
if len(cookies) == 0 {
6685
return nil

0 commit comments

Comments
 (0)