From b6a28421af5c859d19b031b2056d2e97309250b1 Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 20 Jan 2016 15:33:17 +0100 Subject: [PATCH] Extract concatenation into own interface --- concat_test.go | 45 ++++++++++++++++--------------------- datastore.go | 15 +++++++++++++ filestore/filestore.go | 28 +++++++++++++++++++++++ filestore/filestore_test.go | 1 + options_test.go | 2 +- terminate_test.go | 2 +- unrouted_handler.go | 41 ++++++++++++--------------------- 7 files changed, 79 insertions(+), 55 deletions(-) diff --git a/concat_test.go b/concat_test.go index 093107f..1b942cc 100644 --- a/concat_test.go +++ b/concat_test.go @@ -1,11 +1,8 @@ package tusd_test import ( - "io" - "io/ioutil" "net/http" "reflect" - "strings" "testing" . "github.com/tus/tusd" @@ -38,6 +35,10 @@ func (s concatPartialStore) GetInfo(id string) (FileInfo, error) { }, nil } +func (s concatPartialStore) ConcatUploads(id string, uploads []string) error { + return nil +} + func TestConcatPartial(t *testing.T) { handler, _ := NewHandler(Config{ MaxSize: 400, @@ -47,6 +48,16 @@ func TestConcatPartial(t *testing.T) { }, }) + (&httpTest{ + Name: "Successful OPTIONS request", + Method: "OPTIONS", + URL: "", + ResHeader: map[string]string{ + "Tus-Extension": "creation,concatenation", + }, + Code: http.StatusNoContent, + }).Run(handler, t) + (&httpTest{ Name: "Successful POST request", Method: "POST", @@ -122,33 +133,15 @@ func (s concatFinalStore) GetInfo(id string) (FileInfo, error) { return FileInfo{}, ErrNotFound } -func (s concatFinalStore) GetReader(id string) (io.Reader, error) { - if id == "a" { - return strings.NewReader("hello"), nil - } - - if id == "b" { - return strings.NewReader("world"), nil - } - - return nil, ErrNotFound -} - -func (s concatFinalStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) { +func (s concatFinalStore) ConcatUploads(id string, uploads []string) error { if id != "foo" { - s.t.Error("unexpected file id") + s.t.Error("expected final file id to be foo") } - if offset != 0 { - s.t.Error("expected offset to be 0") + if !reflect.DeepEqual(uploads, []string{"a", "b"}) { + s.t.Errorf("expected Concatenating uploads to be a and b") } - - b, _ := ioutil.ReadAll(src) - if string(b) != "helloworld" { - s.t.Error("unexpected content") - } - - return 10, nil + return nil } func TestConcatFinal(t *testing.T) { diff --git a/datastore.go b/datastore.go index 214eae9..70ac552 100644 --- a/datastore.go +++ b/datastore.go @@ -107,3 +107,18 @@ type GetReaderDataStore interface { // be returned. GetReader(id string) (io.Reader, error) } + +// ConcaterDataStore is the interface required to be implemented if the +// Concatenation extension should be enabled. Only in this case, the handler +// will parse and respect the Upload-Concat header. +type ConcaterDataStore interface { + DataStore + + // ConcatUploads concatenations the content from the provided partial uploads + // and write the result in the destination upload which is specified by its + // ID. The caller (usually the handler) must and will ensure that this + // destination upload has been created before with enough space to hold all + // partial uploads. The order, in which the partial uploads are supplied, + // must be respected during concatenation. + ConcatUploads(destination string, partialUploads []string) error +} diff --git a/filestore/filestore.go b/filestore/filestore.go index 268c28c..163a5d6 100644 --- a/filestore/filestore.go +++ b/filestore/filestore.go @@ -102,6 +102,34 @@ func (store FileStore) Terminate(id string) error { return nil } +func (store FileStore) ConcatUploads(dest string, uploads []string) (err error) { + file, err := os.OpenFile(store.binPath(dest), os.O_WRONLY|os.O_APPEND, defaultFilePerm) + if err != nil { + return err + } + defer file.Close() + + var bytesRead int64 + defer func() { + err = store.setOffset(dest, bytesRead) + }() + + for _, id := range uploads { + src, err := store.GetReader(id) + if err != nil { + return err + } + + n, err := io.Copy(file, src) + bytesRead += n + if err != nil { + return err + } + } + + return +} + func (store FileStore) LockUpload(id string) error { lock, err := store.newLock(id) if err != nil { diff --git a/filestore/filestore_test.go b/filestore/filestore_test.go index 01654b7..ca9822f 100644 --- a/filestore/filestore_test.go +++ b/filestore/filestore_test.go @@ -15,6 +15,7 @@ var _ tusd.DataStore = FileStore{} var _ tusd.GetReaderDataStore = FileStore{} var _ tusd.TerminaterDataStore = FileStore{} var _ tusd.LockerDataStore = FileStore{} +var _ tusd.ConcaterDataStore = FileStore{} func TestFilestore(t *testing.T) { tmp, err := ioutil.TempDir("", "tusd-filestore-") diff --git a/options_test.go b/options_test.go index 88f73e9..e82eb88 100644 --- a/options_test.go +++ b/options_test.go @@ -17,7 +17,7 @@ func TestOptions(t *testing.T) { Method: "OPTIONS", Code: http.StatusNoContent, ResHeader: map[string]string{ - "Tus-Extension": "creation,concatenation", + "Tus-Extension": "creation", "Tus-Version": "1.0.0", "Tus-Resumable": "1.0.0", "Tus-Max-Size": "400", diff --git a/terminate_test.go b/terminate_test.go index bf858cc..89d1c37 100644 --- a/terminate_test.go +++ b/terminate_test.go @@ -31,7 +31,7 @@ func TestTerminate(t *testing.T) { Method: "OPTIONS", URL: "", ResHeader: map[string]string{ - "Tus-Extension": "creation,concatenation,termination", + "Tus-Extension": "creation,termination", }, Code: http.StatusNoContent, }).Run(handler, t) diff --git a/unrouted_handler.go b/unrouted_handler.go index 83ea194..aba6e24 100644 --- a/unrouted_handler.go +++ b/unrouted_handler.go @@ -110,10 +110,13 @@ func NewUnroutedHandler(config Config) (*UnroutedHandler, error) { } // Only promote extesions using the Tus-Extension header which are implemented - extensions := "creation,concatenation" + extensions := "creation" if _, ok := config.DataStore.(TerminaterDataStore); ok { extensions += ",termination" } + if _, ok := config.DataStore.(ConcaterDataStore); ok { + extensions += ",concatenation" + } handler := &UnroutedHandler{ config: config, @@ -197,8 +200,16 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { // PostFile creates a new file upload using the datastore after validating the // length and parsing the metadata. func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) { + // Only use the proper Upload-Concat header if the concatenation extension + // is even supported by the data store. + var concatHeader string + concatStore, ok := handler.dataStore.(ConcaterDataStore) + if ok { + concatHeader = r.Header.Get("Upload-Concat") + } + // Parse Upload-Concat header - isPartial, isFinal, partialUploads, err := parseConcat(r.Header.Get("Upload-Concat")) + isPartial, isFinal, partialUploads, err := parseConcat(concatHeader) if err != nil { handler.sendError(w, r, err) return @@ -246,7 +257,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) } if isFinal { - if err := handler.fillFinalUpload(id, partialUploads); err != nil { + if err := concatStore.ConcatUploads(id, partialUploads); err != nil { handler.sendError(w, r, err) return } @@ -548,30 +559,6 @@ func (handler *UnroutedHandler) sizeOfUploads(ids []string) (size int64, err err return } -// Fill an empty upload with the content of the uploads by their ids. The data -// will be written in the order as they appear in the slice -func (handler *UnroutedHandler) fillFinalUpload(id string, uploads []string) error { - dataStore, ok := handler.dataStore.(GetReaderDataStore) - if !ok { - return ErrNotImplemented - } - - readers := make([]io.Reader, len(uploads)) - - for index, uploadID := range uploads { - reader, err := dataStore.GetReader(uploadID) - if err != nil { - return err - } - readers[index] = reader - } - - reader := io.MultiReader(readers...) - _, err := handler.dataStore.WriteChunk(id, 0, reader) - - return err -} - // Parse the Upload-Metadata header as defined in the File Creation extension. // e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n func parseMeta(header string) map[string]string {