add GET route for downloading uploads
This commit is contained in:
parent
04d559dd47
commit
29100e3b5b
|
@ -32,4 +32,10 @@ type DataStore interface {
|
||||||
// requests. It may return an os.ErrNotExist which will be interpretet as a
|
// requests. It may return an os.ErrNotExist which will be interpretet as a
|
||||||
// 404 Not Found.
|
// 404 Not Found.
|
||||||
GetInfo(id string) (FileInfo, error)
|
GetInfo(id string) (FileInfo, error)
|
||||||
|
// Get an io.Reader to allow downloading the file. This feature is not
|
||||||
|
// part of the official tus specification. If this additional function
|
||||||
|
// should not be enabled any call to GetReader should return
|
||||||
|
// tusd.ErrNotImplemented. The length of the resource is determined by
|
||||||
|
// retrieving the offset using GetInfo.
|
||||||
|
GetReader(id string) (io.Reader, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,10 @@ func (store FileStore) GetInfo(id string) (tusd.FileInfo, error) {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store FileStore) GetReader(id string) (io.Reader, error) {
|
||||||
|
return os.Open(store.binPath(id))
|
||||||
|
}
|
||||||
|
|
||||||
// Return the path to the .bin storing the binary data
|
// Return the path to the .bin storing the binary data
|
||||||
func (store FileStore) binPath(id string) string {
|
func (store FileStore) binPath(id string) string {
|
||||||
return store.Path + "/" + id + ".bin"
|
return store.Path + "/" + id + ".bin"
|
||||||
|
|
52
handler.go
52
handler.go
|
@ -25,6 +25,7 @@ var (
|
||||||
ErrFileLocked = errors.New("file currently locked")
|
ErrFileLocked = errors.New("file currently locked")
|
||||||
ErrIllegalOffset = errors.New("illegal offset")
|
ErrIllegalOffset = errors.New("illegal offset")
|
||||||
ErrSizeExceeded = errors.New("resource's size exceeded")
|
ErrSizeExceeded = errors.New("resource's size exceeded")
|
||||||
|
ErrNotImplemented = errors.New("feature not implemented")
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP status codes sent in the response when the specific error is returned.
|
// HTTP status codes sent in the response when the specific error is returned.
|
||||||
|
@ -37,6 +38,7 @@ var ErrStatusCodes = map[error]int{
|
||||||
ErrFileLocked: 423, // Locked (WebDAV) (RFC 4918)
|
ErrFileLocked: 423, // Locked (WebDAV) (RFC 4918)
|
||||||
ErrIllegalOffset: http.StatusConflict,
|
ErrIllegalOffset: http.StatusConflict,
|
||||||
ErrSizeExceeded: http.StatusRequestEntityTooLarge,
|
ErrSizeExceeded: http.StatusRequestEntityTooLarge,
|
||||||
|
ErrNotImplemented: http.StatusNotImplemented,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -92,6 +94,7 @@ func NewHandler(config Config) (*Handler, error) {
|
||||||
|
|
||||||
mux.Post("", http.HandlerFunc(handler.postFile))
|
mux.Post("", http.HandlerFunc(handler.postFile))
|
||||||
mux.Head(":id", http.HandlerFunc(handler.headFile))
|
mux.Head(":id", http.HandlerFunc(handler.headFile))
|
||||||
|
mux.Get(":id", http.HandlerFunc(handler.getFile))
|
||||||
mux.Add("PATCH", ":id", http.HandlerFunc(handler.patchFile))
|
mux.Add("PATCH", ":id", http.HandlerFunc(handler.patchFile))
|
||||||
|
|
||||||
return handler, nil
|
return handler, nil
|
||||||
|
@ -136,7 +139,9 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if the version sent by the client is supported
|
// Test if the version sent by the client is supported
|
||||||
if r.Header.Get("TUS-Resumable") != "1.0.0" {
|
// GET methods are not checked since a browser may visit this URL and does
|
||||||
|
// not include this header. This request is not part of the specification.
|
||||||
|
if r.Method != "GET" && r.Header.Get("TUS-Resumable") != "1.0.0" {
|
||||||
handler.sendError(w, ErrUnsupportedVersion)
|
handler.sendError(w, ErrUnsupportedVersion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -258,6 +263,51 @@ func (handler *Handler) patchFile(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download a file using a GET request. This is not part of the specification.
|
||||||
|
func (handler *Handler) getFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.URL.Query().Get(":id")
|
||||||
|
|
||||||
|
// Ensure file is not locked
|
||||||
|
if _, ok := handler.locks[id]; ok {
|
||||||
|
handler.sendError(w, ErrFileLocked)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock file for further writes (heads are allowed)
|
||||||
|
handler.locks[id] = true
|
||||||
|
|
||||||
|
// File will be unlocked regardless of an error or success
|
||||||
|
defer func() {
|
||||||
|
delete(handler.locks, id)
|
||||||
|
}()
|
||||||
|
|
||||||
|
info, err := handler.dataStore.GetInfo(id)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = ErrNotFound
|
||||||
|
}
|
||||||
|
handler.sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not do anything if no data is stored yet.
|
||||||
|
if info.Offset == 0 {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get reader
|
||||||
|
src, err := handler.dataStore.GetReader(id)
|
||||||
|
if err != nil {
|
||||||
|
handler.sendError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(info.Offset, 10))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
io.Copy(w, src)
|
||||||
|
}
|
||||||
|
|
||||||
// Send the error in the response body. The status code will be looked up in
|
// Send the error in the response body. The status code will be looked up in
|
||||||
// ErrStatusCodes. If none is found 500 Internal Error will be used.
|
// ErrStatusCodes. If none is found 500 Internal Error will be used.
|
||||||
func (handler *Handler) sendError(w http.ResponseWriter, err error) {
|
func (handler *Handler) sendError(w http.ResponseWriter, err error) {
|
||||||
|
|
|
@ -23,6 +23,10 @@ func (store zeroStore) GetInfo(id string) (FileInfo, error) {
|
||||||
return FileInfo{}, nil
|
return FileInfo{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store zeroStore) GetReader(id string) (io.Reader, error) {
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
func TestCORS(t *testing.T) {
|
func TestCORS(t *testing.T) {
|
||||||
handler, _ := NewHandler(Config{})
|
handler, _ := NewHandler(Config{})
|
||||||
|
|
||||||
|
@ -92,7 +96,7 @@ func TestProtocolDiscovery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid or unsupported version
|
// Invalid or unsupported version
|
||||||
req, _ = http.NewRequest("GET", "", nil)
|
req, _ = http.NewRequest("POST", "", nil)
|
||||||
req.Header.Set("TUS-Resumable", "foo")
|
req.Header.Set("TUS-Resumable", "foo")
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
handler.ServeHTTP(w, req)
|
handler.ServeHTTP(w, req)
|
||||||
|
@ -393,3 +397,44 @@ func TestPatchOverflow(t *testing.T) {
|
||||||
t.Errorf("Expected %v (got %v)", http.StatusNoContent, w.Code)
|
t.Errorf("Expected %v (got %v)", http.StatusNoContent, w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type getStore struct {
|
||||||
|
zeroStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s getStore) GetInfo(id string) (FileInfo, error) {
|
||||||
|
if id != "yes" {
|
||||||
|
return FileInfo{}, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileInfo{
|
||||||
|
Offset: 5,
|
||||||
|
Size: 20,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s getStore) GetReader(id string) (io.Reader, error) {
|
||||||
|
return strings.NewReader("hello"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFile(t *testing.T) {
|
||||||
|
handler, _ := NewHandler(Config{
|
||||||
|
DataStore: getStore{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test successfull download
|
||||||
|
req, _ := http.NewRequest("GET", "yes", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected %v (got %v)", http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(w.Body.Bytes()) != "hello" {
|
||||||
|
t.Errorf("Expected response body to be 'hello'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.HeaderMap.Get("Content-Length") != "5" {
|
||||||
|
t.Errorf("Expected Content-Length to be 5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue