From 29100e3b5b1e58f7d9015098b2a0e18aa53b1c17 Mon Sep 17 00:00:00 2001 From: Acconut Date: Fri, 6 Feb 2015 22:05:33 +0100 Subject: [PATCH] add GET route for downloading uploads --- datastore.go | 6 +++++ filestore/filestore.go | 4 ++++ handler.go | 52 +++++++++++++++++++++++++++++++++++++++++- handler_test.go | 47 +++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/datastore.go b/datastore.go index 9c1d365..ed3788f 100644 --- a/datastore.go +++ b/datastore.go @@ -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) } diff --git a/filestore/filestore.go b/filestore/filestore.go index 81414fa..f428ec6 100644 --- a/filestore/filestore.go +++ b/filestore/filestore.go @@ -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" diff --git a/handler.go b/handler.go index da196a6..a67f047 100644 --- a/handler.go +++ b/handler.go @@ -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) { diff --git a/handler_test.go b/handler_test.go index d33b880..f387097 100644 --- a/handler_test.go +++ b/handler_test.go @@ -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") + } +}