Extract concatenation into own interface
This commit is contained in:
parent
bfde73ff89
commit
b6a28421af
|
@ -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) {
|
||||||
|
|
15
datastore.go
15
datastore.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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-")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue