From af14655d940cfcc236eae9ea370098b40bbb3e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Geisendo=CC=88rfer?= Date: Wed, 8 May 2013 11:22:31 +0200 Subject: [PATCH] Initial PATCH implementation --- src/http/data_store.go | 13 +--- src/http/handler.go | 47 ++++++++++--- src/http/handler_test.go | 143 ++++++++++++++++++++++++++++----------- 3 files changed, 145 insertions(+), 58 deletions(-) diff --git a/src/http/data_store.go b/src/http/data_store.go index 9189fa1..6f4c95a 100644 --- a/src/http/data_store.go +++ b/src/http/data_store.go @@ -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 @@ -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) { diff --git a/src/http/handler.go b/src/http/handler.go index 17709ef..a898c42 100644 --- a/src/http/handler.go +++ b/src/http/handler.go @@ -2,6 +2,7 @@ package http import ( "errors" + "fmt" "io" "net/http" "os" @@ -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 { @@ -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 } @@ -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. @@ -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"). // diff --git a/src/http/handler_test.go b/src/http/handler_test.go index ed869b8..3b0bf3d 100644 --- a/src/http/handler_test.go +++ b/src/http/handler_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os" "regexp" + "strings" "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 // to the struct fields below. type TestRequest struct { @@ -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 } @@ -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{