add GET route for downloading uploads

This commit is contained in:
Acconut 2015-02-06 22:05:33 +01:00
parent 04d559dd47
commit 29100e3b5b
4 changed files with 107 additions and 2 deletions

View File

@ -32,4 +32,10 @@ type DataStore interface {
// requests. It may return an os.ErrNotExist which will be interpretet as a
// 404 Not Found.
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)
}

View File

@ -73,6 +73,10 @@ func (store FileStore) GetInfo(id string) (tusd.FileInfo, error) {
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
func (store FileStore) binPath(id string) string {
return store.Path + "/" + id + ".bin"

View File

@ -25,6 +25,7 @@ var (
ErrFileLocked = errors.New("file currently locked")
ErrIllegalOffset = errors.New("illegal offset")
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.
@ -37,6 +38,7 @@ var ErrStatusCodes = map[error]int{
ErrFileLocked: 423, // Locked (WebDAV) (RFC 4918)
ErrIllegalOffset: http.StatusConflict,
ErrSizeExceeded: http.StatusRequestEntityTooLarge,
ErrNotImplemented: http.StatusNotImplemented,
}
type Config struct {
@ -92,6 +94,7 @@ func NewHandler(config Config) (*Handler, error) {
mux.Post("", http.HandlerFunc(handler.postFile))
mux.Head(":id", http.HandlerFunc(handler.headFile))
mux.Get(":id", http.HandlerFunc(handler.getFile))
mux.Add("PATCH", ":id", http.HandlerFunc(handler.patchFile))
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
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)
return
}
@ -258,6 +263,51 @@ func (handler *Handler) patchFile(w http.ResponseWriter, r *http.Request) {
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
// ErrStatusCodes. If none is found 500 Internal Error will be used.
func (handler *Handler) sendError(w http.ResponseWriter, err error) {

View File

@ -23,6 +23,10 @@ func (store zeroStore) GetInfo(id string) (FileInfo, error) {
return FileInfo{}, nil
}
func (store zeroStore) GetReader(id string) (io.Reader, error) {
return nil, ErrNotImplemented
}
func TestCORS(t *testing.T) {
handler, _ := NewHandler(Config{})
@ -92,7 +96,7 @@ func TestProtocolDiscovery(t *testing.T) {
}
// Invalid or unsupported version
req, _ = http.NewRequest("GET", "", nil)
req, _ = http.NewRequest("POST", "", nil)
req.Header.Set("TUS-Resumable", "foo")
w = httptest.NewRecorder()
handler.ServeHTTP(w, req)
@ -393,3 +397,44 @@ func TestPatchOverflow(t *testing.T) {
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")
}
}