Extract concatenation into own interface

This commit is contained in:
Marius 2016-01-20 15:33:17 +01:00
parent bfde73ff89
commit b6a28421af
7 changed files with 79 additions and 55 deletions

View File

@ -1,11 +1,8 @@
package tusd_test package tusd_test
import ( import (
"io"
"io/ioutil"
"net/http" "net/http"
"reflect" "reflect"
"strings"
"testing" "testing"
. "github.com/tus/tusd" . "github.com/tus/tusd"
@ -38,6 +35,10 @@ func (s concatPartialStore) GetInfo(id string) (FileInfo, error) {
}, nil }, nil
} }
func (s concatPartialStore) ConcatUploads(id string, uploads []string) error {
return nil
}
func TestConcatPartial(t *testing.T) { func TestConcatPartial(t *testing.T) {
handler, _ := NewHandler(Config{ handler, _ := NewHandler(Config{
MaxSize: 400, 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{ (&httpTest{
Name: "Successful POST request", Name: "Successful POST request",
Method: "POST", Method: "POST",
@ -122,33 +133,15 @@ func (s concatFinalStore) GetInfo(id string) (FileInfo, error) {
return FileInfo{}, ErrNotFound return FileInfo{}, ErrNotFound
} }
func (s concatFinalStore) GetReader(id string) (io.Reader, error) { func (s concatFinalStore) ConcatUploads(id string, uploads []string) 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) {
if id != "foo" { if id != "foo" {
s.t.Error("unexpected file id") s.t.Error("expected final file id to be foo")
} }
if offset != 0 { if !reflect.DeepEqual(uploads, []string{"a", "b"}) {
s.t.Error("expected offset to be 0") s.t.Errorf("expected Concatenating uploads to be a and b")
} }
return nil
b, _ := ioutil.ReadAll(src)
if string(b) != "helloworld" {
s.t.Error("unexpected content")
}
return 10, nil
} }
func TestConcatFinal(t *testing.T) { func TestConcatFinal(t *testing.T) {

View File

@ -107,3 +107,18 @@ type GetReaderDataStore interface {
// be returned. // be returned.
GetReader(id string) (io.Reader, error) 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
}

View File

@ -102,6 +102,34 @@ func (store FileStore) Terminate(id string) error {
return nil 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 { func (store FileStore) LockUpload(id string) error {
lock, err := store.newLock(id) lock, err := store.newLock(id)
if err != nil { if err != nil {

View File

@ -15,6 +15,7 @@ var _ tusd.DataStore = FileStore{}
var _ tusd.GetReaderDataStore = FileStore{} var _ tusd.GetReaderDataStore = FileStore{}
var _ tusd.TerminaterDataStore = FileStore{} var _ tusd.TerminaterDataStore = FileStore{}
var _ tusd.LockerDataStore = FileStore{} var _ tusd.LockerDataStore = FileStore{}
var _ tusd.ConcaterDataStore = FileStore{}
func TestFilestore(t *testing.T) { func TestFilestore(t *testing.T) {
tmp, err := ioutil.TempDir("", "tusd-filestore-") tmp, err := ioutil.TempDir("", "tusd-filestore-")

View File

@ -17,7 +17,7 @@ func TestOptions(t *testing.T) {
Method: "OPTIONS", Method: "OPTIONS",
Code: http.StatusNoContent, Code: http.StatusNoContent,
ResHeader: map[string]string{ ResHeader: map[string]string{
"Tus-Extension": "creation,concatenation", "Tus-Extension": "creation",
"Tus-Version": "1.0.0", "Tus-Version": "1.0.0",
"Tus-Resumable": "1.0.0", "Tus-Resumable": "1.0.0",
"Tus-Max-Size": "400", "Tus-Max-Size": "400",

View File

@ -31,7 +31,7 @@ func TestTerminate(t *testing.T) {
Method: "OPTIONS", Method: "OPTIONS",
URL: "", URL: "",
ResHeader: map[string]string{ ResHeader: map[string]string{
"Tus-Extension": "creation,concatenation,termination", "Tus-Extension": "creation,termination",
}, },
Code: http.StatusNoContent, Code: http.StatusNoContent,
}).Run(handler, t) }).Run(handler, t)

View File

@ -110,10 +110,13 @@ func NewUnroutedHandler(config Config) (*UnroutedHandler, error) {
} }
// Only promote extesions using the Tus-Extension header which are implemented // Only promote extesions using the Tus-Extension header which are implemented
extensions := "creation,concatenation" extensions := "creation"
if _, ok := config.DataStore.(TerminaterDataStore); ok { if _, ok := config.DataStore.(TerminaterDataStore); ok {
extensions += ",termination" extensions += ",termination"
} }
if _, ok := config.DataStore.(ConcaterDataStore); ok {
extensions += ",concatenation"
}
handler := &UnroutedHandler{ handler := &UnroutedHandler{
config: config, 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 // PostFile creates a new file upload using the datastore after validating the
// length and parsing the metadata. // length and parsing the metadata.
func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) { 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 // Parse Upload-Concat header
isPartial, isFinal, partialUploads, err := parseConcat(r.Header.Get("Upload-Concat")) isPartial, isFinal, partialUploads, err := parseConcat(concatHeader)
if err != nil { if err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
@ -246,7 +257,7 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request)
} }
if isFinal { if isFinal {
if err := handler.fillFinalUpload(id, partialUploads); err != nil { if err := concatStore.ConcatUploads(id, partialUploads); err != nil {
handler.sendError(w, r, err) handler.sendError(w, r, err)
return return
} }
@ -548,30 +559,6 @@ func (handler *UnroutedHandler) sizeOfUploads(ids []string) (size int64, err err
return 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. // Parse the Upload-Metadata header as defined in the File Creation extension.
// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n // e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n
func parseMeta(header string) map[string]string { func parseMeta(header string) map[string]string {