Initial PATCH implementation
This commit is contained in:
parent
96e431cfda
commit
af14655d94
|
@ -41,7 +41,7 @@ func (s *DataStore) CreateFile(id string, size int64, meta map[string]string) er
|
||||||
return s.appendFileLog(id, entry)
|
return s.appendFileLog(id, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DataStore) WriteFileChunk(id string, start int64, end int64, src io.Reader) error {
|
func (s *DataStore) WriteFileChunk(id string, start int64, src io.Reader) error {
|
||||||
file, err := os.OpenFile(s.filePath(id), os.O_WRONLY, 0666)
|
file, err := os.OpenFile(s.filePath(id), os.O_WRONLY, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -54,21 +54,14 @@ func (s *DataStore) WriteFileChunk(id string, start int64, end int64, src io.Rea
|
||||||
return errors.New("WriteFileChunk: seek failure")
|
return errors.New("WriteFileChunk: seek failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
size := end - start + 1
|
n, err := io.Copy(file, src)
|
||||||
n, err := io.CopyN(file, src, size)
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
entry := logEntry{Chunk: &chunkEntry{Start: start, End: start + n - 1}}
|
entry := logEntry{Chunk: &chunkEntry{Start: start, End: start + n - 1}}
|
||||||
if err := s.appendFileLog(id, entry); err != nil {
|
if err := s.appendFileLog(id, entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if n != size {
|
|
||||||
return errors.New("WriteFileChunk: partial copy")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DataStore) GetFileMeta(id string) (*fileMeta, error) {
|
func (s *DataStore) GetFileMeta(id string) (*fileMeta, error) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -11,7 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fileUrlMatcher = regexp.MustCompilePOSIX("^/([a-z0-9]{32})$")
|
var fileUrlMatcher = regexp.MustCompile("^/([a-z0-9]{32})$")
|
||||||
|
|
||||||
// HandlerConfig holds the configuration for a tus Handler.
|
// HandlerConfig holds the configuration for a tus Handler.
|
||||||
type HandlerConfig struct {
|
type HandlerConfig struct {
|
||||||
|
@ -88,15 +89,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches := fileUrlMatcher.FindStringSubmatch(relPath); matches != nil {
|
if matches := fileUrlMatcher.FindStringSubmatch(relPath); matches != nil {
|
||||||
//id := matches[1]
|
id := matches[1]
|
||||||
if r.Method == "PATCH" {
|
if r.Method == "PATCH" {
|
||||||
|
h.patchFile(w, r, id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle invalid method
|
// handle invalid method
|
||||||
allowed := "PATCH"
|
allowed := "PATCH"
|
||||||
w.Header().Set("Allow", allowed)
|
w.Header().Set("Allow", allowed)
|
||||||
err := errors.New(r.Method + " used against file creation url. Allowed: "+allowed)
|
err := errors.New(r.Method + " used against file creation url. Allowed: " + allowed)
|
||||||
h.err(err, w, http.StatusMethodNotAllowed)
|
h.err(err, w, http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -109,18 +111,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
|
||||||
id := uid()
|
id := uid()
|
||||||
|
|
||||||
finalLength, err := strconv.ParseInt(r.Header.Get("Final-Length"), 10, 64)
|
finalLength, err := getPositiveIntHeader(r, "Final-Length")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.New("invalid Final-Length header: " + err.Error())
|
|
||||||
h.err(err, w, http.StatusBadRequest)
|
h.err(err, w, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if finalLength < 0 {
|
|
||||||
h.err(errors.New("negative Final-Length values not supported"), w, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Define meta data extension and implement it here
|
// @TODO: Define meta data extension and implement it here
|
||||||
// @TODO: Make max finalLength configurable, reply with error if exceeded.
|
// @TODO: Make max finalLength configurable, reply with error if exceeded.
|
||||||
// This should go into the protocol as well.
|
// This should go into the protocol as well.
|
||||||
|
@ -133,6 +129,37 @@ func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) patchFile(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
offset, err := getPositiveIntHeader(r, "Offset")
|
||||||
|
if err != nil {
|
||||||
|
h.err(err, w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.store.WriteFileChunk(id, offset, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
h.err(err, w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("success\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPositiveIntHeader(r *http.Request, key string) (int64, error) {
|
||||||
|
val := r.Header.Get(key)
|
||||||
|
if val == "" {
|
||||||
|
return 0, errors.New(key+" header must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
intVal, err := strconv.ParseInt(val, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("invalid " + key + " header: " + err.Error())
|
||||||
|
} else if intVal < 0 {
|
||||||
|
return 0, errors.New(key + " header must be > 0")
|
||||||
|
}
|
||||||
|
return intVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
// absUrl turn a relPath (e.g. "/foo") into an absolute url (e.g.
|
// absUrl turn a relPath (e.g. "/foo") into an absolute url (e.g.
|
||||||
// "http://example.com/foo").
|
// "http://example.com/foo").
|
||||||
//
|
//
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -114,6 +115,108 @@ func TestProtocol_FileCreation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var Protocol_Core_Tests = []struct {
|
||||||
|
Description string
|
||||||
|
FinalLength int64
|
||||||
|
Requests []TestRequest
|
||||||
|
ExpectFileContent string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Description: "Bad method",
|
||||||
|
FinalLength: 1024,
|
||||||
|
Requests: []TestRequest{
|
||||||
|
{
|
||||||
|
Method: "PUT",
|
||||||
|
ExpectStatusCode: http.StatusMethodNotAllowed,
|
||||||
|
ExpectHeaders: map[string]string{"Allow": "PATCH"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Missing Offset header",
|
||||||
|
FinalLength: 5,
|
||||||
|
Requests: []TestRequest{
|
||||||
|
{Method: "PATCH", Body: "hello", ExpectStatusCode: http.StatusBadRequest},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Negative Offset header",
|
||||||
|
FinalLength: 5,
|
||||||
|
Requests: []TestRequest{
|
||||||
|
{
|
||||||
|
Method: "PATCH",
|
||||||
|
Headers: map[string]string{"Offset": "-10"},
|
||||||
|
Body: "hello",
|
||||||
|
ExpectStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Invalid Offset header",
|
||||||
|
FinalLength: 5,
|
||||||
|
Requests: []TestRequest{
|
||||||
|
{
|
||||||
|
Method: "PATCH",
|
||||||
|
Headers: map[string]string{"Offset": "lalala"},
|
||||||
|
Body: "hello",
|
||||||
|
ExpectStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Single PATCH Upload",
|
||||||
|
FinalLength: 5,
|
||||||
|
ExpectFileContent: "hello",
|
||||||
|
Requests: []TestRequest{
|
||||||
|
{
|
||||||
|
Method: "PATCH",
|
||||||
|
Headers: map[string]string{"Offset": "0"},
|
||||||
|
Body: "hello",
|
||||||
|
ExpectStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtocol_Core(t *testing.T) {
|
||||||
|
setup := Setup()
|
||||||
|
defer setup.Teardown()
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
for _, test := range Protocol_Core_Tests {
|
||||||
|
t.Logf("test: %s", test.Description)
|
||||||
|
|
||||||
|
location := createFile(setup, test.FinalLength)
|
||||||
|
for _, request := range test.Requests {
|
||||||
|
request.Url = location
|
||||||
|
if err := request.Do(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue Tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ExpectFileContent != "" {
|
||||||
|
id := regexp.MustCompile("[a-z0-9]{32}$").FindString(location)
|
||||||
|
reader, err := setup.Handler.store.ReadFile(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue Tests
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue Tests
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(content) != test.ExpectFileContent {
|
||||||
|
t.Errorf("expected content: %s, got: %s", test.ExpectFileContent, content)
|
||||||
|
continue Tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestRequest is a test helper that performs and validates requests according
|
// TestRequest is a test helper that performs and validates requests according
|
||||||
// to the struct fields below.
|
// to the struct fields below.
|
||||||
type TestRequest struct {
|
type TestRequest struct {
|
||||||
|
@ -124,10 +227,11 @@ type TestRequest struct {
|
||||||
ExpectHeaders map[string]string
|
ExpectHeaders map[string]string
|
||||||
MatchHeaders map[string]*regexp.Regexp
|
MatchHeaders map[string]*regexp.Regexp
|
||||||
Response *http.Response
|
Response *http.Response
|
||||||
|
Body string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *TestRequest) Do() error {
|
func (r *TestRequest) Do() error {
|
||||||
req, err := http.NewRequest(r.Method, r.Url, nil)
|
req, err := http.NewRequest(r.Method, r.Url, strings.NewReader(r.Body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -164,43 +268,6 @@ func (r *TestRequest) Do() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var Protocol_Core_Tests = []struct {
|
|
||||||
Description string
|
|
||||||
FinalLength int64
|
|
||||||
Requests []TestRequest
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Description: "Bad method",
|
|
||||||
FinalLength: 1024,
|
|
||||||
Requests: []TestRequest{
|
|
||||||
{
|
|
||||||
Method: "PUT",
|
|
||||||
ExpectStatusCode: http.StatusMethodNotAllowed,
|
|
||||||
ExpectHeaders: map[string]string{"Allow": "PATCH"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtocol_Core(t *testing.T) {
|
|
||||||
setup := Setup()
|
|
||||||
defer setup.Teardown()
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
for _, test := range Protocol_Core_Tests {
|
|
||||||
t.Logf("test: %s", test.Description)
|
|
||||||
|
|
||||||
location := createFile(setup, test.FinalLength)
|
|
||||||
for _, request := range test.Requests {
|
|
||||||
request.Url = location
|
|
||||||
if err := request.Do(); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
continue Tests
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createFile is a test helper that creates a new file and returns the url.
|
// createFile is a test helper that creates a new file and returns the url.
|
||||||
func createFile(setup *TestSetup, finalLength int64) (url string) {
|
func createFile(setup *TestSetup, finalLength int64) (url string) {
|
||||||
req := TestRequest{
|
req := TestRequest{
|
||||||
|
|
Loading…
Reference in New Issue