Skip to content
This repository was archived by the owner on Jul 10, 2022. It is now read-only.

Commit b21ab59

Browse files
🚀
1 parent 0d1e2b0 commit b21ab59

File tree

12 files changed

+695
-1
lines changed

12 files changed

+695
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ _testmain.go
2222
*.exe
2323
*.test
2424
*.prof
25+
26+
.DS_Store

.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: go
2+
go:
3+
- 1.6
4+
- 1.7
5+
- tip
6+
install:
7+
- go get github.com/stretchr/testify
8+
- go get github.com/tus/tusd
9+
script: go test -v ./...
10+
notifications:
11+
email:
12+
recipients:
13+
14+
on_success: never
15+
on_failure: always

Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM golang:1.7
2+
3+
RUN mkdir -p /go/src/github.com/eventials/go-tus
4+
5+
WORKDIR /go/src/github.com/eventials/go-tus
6+
7+
RUN go get github.com/stretchr/testify
8+
RUN go get github.com/tus/tusd

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,44 @@
11
# go-tus
2-
A pure Go client for the tus resumable upload protocol
2+
3+
A pure Go client for the [tus resumable upload protocol](http://tus.io/)
4+
5+
## Example
6+
7+
```go
8+
c, err := NewClient("http://localhost:1080/files/", "/videos/my-video.mp4", nil)
9+
10+
if err != nil {
11+
panic(err)
12+
}
13+
14+
err = c.Upload()
15+
16+
if err != nil {
17+
panic(err)
18+
}
19+
```
20+
21+
## Features
22+
23+
> This is not a full protocol client implementation.
24+
25+
Checksum, Termination and Concatenation extensions are not implemented yet.
26+
27+
This client allows to resume an upload if a Storage is used.
28+
29+
## Built in Storages
30+
31+
Storages are used to save the progress of an upload.
32+
33+
| Name | Backend | Dependencies |
34+
|:----:|:-------:|:------------:|
35+
| MemoryStorage | In-Memory | None |
36+
37+
## Future Work
38+
39+
- [ ] SQLite storage
40+
- [ ] Redis storage
41+
- [ ] Memcached storage
42+
- [ ] Checksum extension
43+
- [ ] Termination extension
44+
- [ ] Concatenation extension

client.go

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Package tus provides a client to tus protocol version 1.0.0.
2+
//
3+
// tus is a protocol based on HTTP for resumable file uploads. Resumable means that
4+
// an upload can be interrupted at any moment and can be resumed without
5+
// re-uploading the previous data again. An interruption may happen willingly, if
6+
// the user wants to pause, or by accident in case of an network issue or server
7+
// outage (http://tus.io).
8+
package tus
9+
10+
import (
11+
"bytes"
12+
"fmt"
13+
"math"
14+
"net/http"
15+
"os"
16+
"strconv"
17+
)
18+
19+
// Client represents the tus client.
20+
// You can use it in goroutines to create parallels uploads.
21+
type Client struct {
22+
config *Config
23+
client *http.Client
24+
aborted bool
25+
filename string
26+
url string
27+
protocolVersion string
28+
}
29+
30+
// NewClient creates a new tus client.
31+
func NewClient(url, filename string, config *Config) (*Client, error) {
32+
fi, err := os.Stat(filename)
33+
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
if fi.IsDir() {
39+
return nil, fmt.Errorf("'%s' is a directory.", filename)
40+
}
41+
42+
if config == nil {
43+
config = DefaultConfig()
44+
} else {
45+
if err = config.Validate(); err != nil {
46+
return nil, err
47+
}
48+
}
49+
50+
return &Client{
51+
config: config,
52+
client: &http.Client{},
53+
aborted: false,
54+
filename: filename,
55+
url: url,
56+
protocolVersion: "1.0.0",
57+
}, nil
58+
}
59+
60+
// Upload start the uploading process.
61+
// If resume is enabled and part of the file was uploaded, it will resume the upload.
62+
func (c *Client) Upload() error {
63+
c.config.Logger.Printf("processing upload of '%s'.\n", c.filename)
64+
65+
c.aborted = false
66+
67+
f, err := os.Open(c.filename)
68+
69+
if err != nil {
70+
return err
71+
}
72+
73+
defer f.Close()
74+
75+
defer func() {
76+
if c.config.Resume && !c.aborted {
77+
c.config.Storage.Delete(f)
78+
}
79+
}()
80+
81+
if c.config.Resume {
82+
c.config.Logger.Printf("checking if can resume upload of '%s'.\n", c.filename)
83+
84+
url, ok := c.config.Storage.Get(f)
85+
86+
if ok {
87+
c.config.Logger.Printf("resuming upload of '%s'.\n", c.filename)
88+
89+
offset, err := c.uploadOffset(f, url)
90+
91+
if err != nil {
92+
return err
93+
}
94+
95+
if offset != -1 {
96+
return c.upload(f, url, offset)
97+
}
98+
}
99+
}
100+
101+
url, err := c.create(f)
102+
103+
if err != nil {
104+
return err
105+
}
106+
107+
c.config.Logger.Printf("starting upload of '%s'.\n", c.filename)
108+
109+
if c.config.Resume {
110+
c.config.Storage.Set(f, url)
111+
}
112+
113+
err = c.upload(f, url, 0)
114+
115+
if err == nil {
116+
c.config.Logger.Printf("upload of '%s' completed.\n", c.filename)
117+
} else {
118+
c.config.Logger.Printf("upload of '%s' failed.\n", c.filename)
119+
}
120+
121+
return err
122+
}
123+
124+
// Abort stop the upload process.
125+
// If resume is enabled you can continue the upload later.
126+
func (c *Client) Abort() {
127+
c.config.Logger.Printf("aborting upload of '%s'.\n", c.filename)
128+
c.aborted = true
129+
}
130+
131+
func (c *Client) create(f *os.File) (string, error) {
132+
fileInfo, err := f.Stat()
133+
134+
if err != nil {
135+
return "", err
136+
}
137+
138+
req, err := http.NewRequest("POST", c.url, nil)
139+
140+
if err != nil {
141+
return "", fmt.Errorf("failed to create upload of '%s': %s", err)
142+
}
143+
144+
req.Header.Set("Content-Length", "0")
145+
req.Header.Set("Upload-Length", strconv.FormatInt(fileInfo.Size(), 10))
146+
req.Header.Set("Tus-Resumable", c.protocolVersion)
147+
req.Header.Set("Upload-Metadata", fmt.Sprintf("filename %s", b64encode(fileInfo.Name())))
148+
149+
res, err := c.client.Do(req)
150+
151+
if err != nil {
152+
return "", fmt.Errorf("failed to create upload of '%s': %s", fileInfo.Name(), err)
153+
}
154+
155+
switch res.StatusCode {
156+
case 201:
157+
return res.Header.Get("Location"), nil
158+
case 412:
159+
return "", fmt.Errorf("failed to create upload of '%s': this client is incompatible with Tus sever version %s.", fileInfo.Name(), res.Header.Get("Tus-Version"))
160+
case 413:
161+
return "", fmt.Errorf("failed to create upload of '%s': upload file is to large.", fileInfo.Name())
162+
default:
163+
return "", fmt.Errorf("failed to create upload of '%s': %d", fileInfo.Name(), res.StatusCode)
164+
}
165+
}
166+
167+
func (c *Client) upload(f *os.File, url string, offset int64) error {
168+
fileInfo, err := f.Stat()
169+
170+
if err != nil {
171+
return err
172+
}
173+
174+
fileSize := fileInfo.Size()
175+
totalParts := math.Ceil(float64(fileSize) / float64(c.config.ChunkSize))
176+
177+
for offset < fileSize && !c.aborted {
178+
currentPart := math.Ceil(float64(offset) / float64(c.config.ChunkSize))
179+
c.config.Logger.Printf("uploading file '%s' (%g/%g).\n", c.filename, currentPart+1, totalParts)
180+
181+
_, err := f.Seek(offset, 0)
182+
183+
if err != nil {
184+
return fmt.Errorf("failed to upload '%s': %s", fileInfo.Name(), err)
185+
}
186+
187+
data := make([]byte, c.config.ChunkSize)
188+
size, err := f.Read(data)
189+
190+
if err != nil {
191+
return fmt.Errorf("failed to upload '%s': %s", fileInfo.Name(), err)
192+
}
193+
194+
method := "PATCH"
195+
196+
if c.config.OverridePatchMethod {
197+
method = "POST"
198+
}
199+
200+
req, err := http.NewRequest(method, url, bytes.NewBuffer(data[:size]))
201+
202+
if err != nil {
203+
return fmt.Errorf("failed to upload '%s': %s", fileInfo.Name(), err)
204+
}
205+
206+
req.Header.Set("Content-Type", "application/offset+octet-stream")
207+
req.Header.Set("Content-Length", strconv.Itoa(size))
208+
req.Header.Set("Upload-Offset", strconv.FormatInt(offset, 10))
209+
req.Header.Set("Tus-Resumable", c.protocolVersion)
210+
211+
if c.config.OverridePatchMethod {
212+
req.Header.Set("X-HTTP-Method-Override", "PATCH")
213+
}
214+
215+
res, err := c.client.Do(req)
216+
217+
if err != nil {
218+
return fmt.Errorf("failed to upload '%s': %s", err)
219+
}
220+
221+
switch res.StatusCode {
222+
case 204:
223+
offset, err = strconv.ParseInt(res.Header.Get("Upload-Offset"), 10, 64)
224+
225+
if err != nil {
226+
return fmt.Errorf("failed to upload '%s': can't parse upload offset.", fileInfo.Name())
227+
}
228+
case 409:
229+
return fmt.Errorf("failed to upload '%s': upload offset doesn't match.", fileInfo.Name())
230+
case 412:
231+
return fmt.Errorf("failed to upload '%s': this client is incompatible with Tus server version %s.", fileInfo.Name(), res.Header.Get("Tus-Version"))
232+
case 413:
233+
return fmt.Errorf("failed to upload '%s': upload file is to large.", fileInfo.Name())
234+
default:
235+
return fmt.Errorf("failed to upload '%s': %d", fileInfo.Name(), res.StatusCode)
236+
}
237+
}
238+
239+
return nil
240+
}
241+
242+
func (c *Client) uploadOffset(f *os.File, url string) (int64, error) {
243+
fileInfo, err := f.Stat()
244+
245+
if err != nil {
246+
return 0, err
247+
}
248+
249+
req, err := http.NewRequest("HEAD", url, nil)
250+
251+
if err != nil {
252+
return 0, fmt.Errorf("failed to resume upload of '%s': %s", err)
253+
}
254+
255+
req.Header.Set("Tus-Resumable", c.protocolVersion)
256+
257+
res, err := c.client.Do(req)
258+
259+
if err != nil {
260+
return 0, fmt.Errorf("failed to resume upload of '%s': %s", fileInfo.Name(), err)
261+
}
262+
263+
switch res.StatusCode {
264+
case 200:
265+
i, err := strconv.ParseInt(res.Header.Get("Upload-Offset"), 10, 64)
266+
267+
if err == nil {
268+
return i, nil
269+
} else {
270+
return 0, fmt.Errorf("failed to resume upload of '%s': can't parse upload offset.", fileInfo.Name())
271+
}
272+
case 403, 404, 410:
273+
// file doesn't exists.
274+
return -1, nil
275+
case 412:
276+
return 0, fmt.Errorf("failed to resume upload of '%s': this client is incompatible with Tus server version %s.", fileInfo.Name(), res.Header.Get("Tus-Version"))
277+
default:
278+
return 0, fmt.Errorf("failed to resume upload of '%s': %d", fileInfo.Name(), res.StatusCode)
279+
}
280+
}

0 commit comments

Comments
 (0)