Skip to content

Commit

Permalink
Initial PATCH implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
felixge committed May 8, 2013
1 parent 96e431c commit af14655
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 58 deletions.
13 changes: 3 additions & 10 deletions src/http/data_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (s *DataStore) CreateFile(id string, size int64, meta map[string]string) er
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)
if err != nil {
return err
Expand All @@ -54,21 +54,14 @@ func (s *DataStore) WriteFileChunk(id string, start int64, end int64, src io.Rea
return errors.New("WriteFileChunk: seek failure")
}

size := end - start + 1
n, err := io.CopyN(file, src, size)
n, err := io.Copy(file, src)
if n > 0 {
entry := logEntry{Chunk: &chunkEntry{Start: start, End: start + n - 1}}
if err := s.appendFileLog(id, entry); err != nil {
return err
}
}

if err != nil {
return err
} else if n != size {
return errors.New("WriteFileChunk: partial copy")
}
return nil
return err
}

func (s *DataStore) GetFileMeta(id string) (*fileMeta, error) {
Expand Down
47 changes: 37 additions & 10 deletions src/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http

import (
"errors"
"fmt"
"io"
"net/http"
"os"
Expand All @@ -11,7 +12,7 @@ import (
"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.
type HandlerConfig struct {
Expand Down Expand Up @@ -88,15 +89,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

if matches := fileUrlMatcher.FindStringSubmatch(relPath); matches != nil {
//id := matches[1]
id := matches[1]
if r.Method == "PATCH" {
h.patchFile(w, r, id)
return
}

// handle invalid method
allowed := "PATCH"
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)
return
}
Expand All @@ -109,18 +111,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
id := uid()

finalLength, err := strconv.ParseInt(r.Header.Get("Final-Length"), 10, 64)
finalLength, err := getPositiveIntHeader(r, "Final-Length")
if err != nil {
err = errors.New("invalid Final-Length header: " + err.Error())
h.err(err, w, http.StatusBadRequest)
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: Make max finalLength configurable, reply with error if exceeded.
// This should go into the protocol as well.
Expand All @@ -133,6 +129,37 @@ func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
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.
// "http://example.com/foo").
//
Expand Down
143 changes: 105 additions & 38 deletions src/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http/httptest"
"os"
"regexp"
"strings"
"testing"
)

Expand Down Expand Up @@ -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
// to the struct fields below.
type TestRequest struct {
Expand All @@ -124,10 +227,11 @@ type TestRequest struct {
ExpectHeaders map[string]string
MatchHeaders map[string]*regexp.Regexp
Response *http.Response
Body string
}

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 {
return err
}
Expand Down Expand Up @@ -164,43 +268,6 @@ func (r *TestRequest) Do() error {
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.
func createFile(setup *TestSetup, finalLength int64) (url string) {
req := TestRequest{
Expand Down

0 comments on commit af14655

Please sign in to comment.