Skip to content

Commit

Permalink
Add form data upload (#521)
Browse files Browse the repository at this point in the history
* Add form data upload

* Fix formatting and suggested nit
  • Loading branch information
Sytten authored Jul 13, 2021
1 parent cd4471f commit 3aa1429
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
10 changes: 10 additions & 0 deletions fakestorage/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -209,6 +210,10 @@ func (s *Server) buildMuxer() {
s.mux.Host(s.publicHost).Path("/{bucketName}/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)
s.mux.Host("{bucketName:.+}").Path("/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)

// Form Uploads
s.mux.Host(s.publicHost).Path("/{bucketName}").MatcherFunc(matchFormData).Methods("POST", "PUT").HandlerFunc(xmlToHTTPHandler(s.insertFormObject))
s.mux.Host(bucketHost).MatcherFunc(matchFormData).Methods("POST", "PUT").HandlerFunc(xmlToHTTPHandler(s.insertFormObject))

// Signed URL Uploads
s.mux.Host(s.publicHost).Path("/{bucketName}/{objectName:.+}").Methods("POST", "PUT").HandlerFunc(jsonToHTTPHandler(s.insertObject))
s.mux.Host(bucketHost).Path("/{objectName:.+}").Methods("POST", "PUT").HandlerFunc(jsonToHTTPHandler(s.insertObject))
Expand Down Expand Up @@ -272,3 +277,8 @@ func requestCompressHandler(h http.Handler) http.Handler {
h.ServeHTTP(w, r)
})
}

func matchFormData(r *http.Request, _ *mux.RouteMatch) bool {
contentType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
return contentType == "multipart/form-data"
}
69 changes: 69 additions & 0 deletions fakestorage/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,75 @@ func (s *Server) insertObject(r *http.Request) jsonResponse {
}
}

func (s *Server) insertFormObject(r *http.Request) xmlResponse {
bucketName := mux.Vars(r)["bucketName"]

if err := r.ParseMultipartForm(32 << 20); nil != err {
return xmlResponse{errorMessage: "invalid form", status: http.StatusBadRequest}
}

// Load metadata
var name string
if keys, ok := r.MultipartForm.Value["key"]; ok {
name = keys[0]
}
if name == "" {
return xmlResponse{errorMessage: "missing key", status: http.StatusBadRequest}
}
var predefinedACL string
if acls, ok := r.MultipartForm.Value["acl"]; ok {
predefinedACL = acls[0]
}
var contentEncoding string
if contentEncodings, ok := r.MultipartForm.Value["Content-Encoding"]; ok {
contentEncoding = contentEncodings[0]
}
var contentType string
if contentTypes, ok := r.MultipartForm.Value["Content-Type"]; ok {
contentType = contentTypes[0]
}
metaData := make(map[string]string)
for key := range r.MultipartForm.Value {
lowerKey := strings.ToLower(key)
if metaDataKey := strings.TrimPrefix(lowerKey, "x-goog-meta-"); metaDataKey != lowerKey {
metaData[metaDataKey] = r.MultipartForm.Value[key][0]
}
}

// Load file
var file *multipart.FileHeader
if files, ok := r.MultipartForm.File["file"]; ok {
file = files[0]
}
if file == nil {
return xmlResponse{errorMessage: "missing file", status: http.StatusBadRequest}
}
infile, err := file.Open()
if err != nil {
return xmlResponse{errorMessage: err.Error()}
}
data, err := ioutil.ReadAll(infile)
if err != nil {
return xmlResponse{errorMessage: err.Error()}
}
obj := Object{
BucketName: bucketName,
Name: name,
Content: data,
ContentType: contentType,
ContentEncoding: contentEncoding,
Crc32c: checksum.EncodedCrc32cChecksum(data),
Md5Hash: checksum.EncodedMd5Hash(data),
ACL: getObjectACL(predefinedACL),
Metadata: metaData,
}
obj, err = s.createObject(obj)
if err != nil {
return xmlResponse{errorMessage: err.Error()}
}
return xmlResponse{status: http.StatusNoContent}
}

func (s *Server) checkUploadPreconditions(r *http.Request, bucketName string, objectName string) *jsonResponse {
ifGenerationMatch := r.URL.Query().Get("ifGenerationMatch")

Expand Down
85 changes: 85 additions & 0 deletions fakestorage/upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"reflect"
"strings"
Expand Down Expand Up @@ -654,6 +655,90 @@ func TestServerGzippedUpload(t *testing.T) {
})
}

func TestFormDataUpload(t *testing.T) {
server, err := NewServerWithOptions(Options{PublicHost: "127.0.0.1"})
if err != nil {
t.Fatalf("could not start server: %v", err)
}
defer server.Stop()
server.CreateBucketWithOpts(CreateBucketOpts{Name: "other-bucket"})

var buf bytes.Buffer
const content = "some weird content"
const contentType = "text/plain"
writer := multipart.NewWriter(&buf)

var fieldWriter io.Writer
if fieldWriter, err = writer.CreateFormField("key"); err != nil {
t.Fatal(err)
}
if _, err := fieldWriter.Write([]byte("object.txt")); err != nil {
t.Fatal(err)
}

if fieldWriter, err = writer.CreateFormField("Content-Type"); err != nil {
t.Fatal(err)
}
if _, err := fieldWriter.Write([]byte(contentType)); err != nil {
t.Fatal(err)
}

if fieldWriter, err = writer.CreateFormField("x-goog-meta-key"); err != nil {
t.Fatal(err)
}
if _, err := fieldWriter.Write([]byte("Value")); err != nil {
t.Fatal(err)
}

if fieldWriter, err = writer.CreateFormFile("file", "object.txt"); err != nil {
t.Fatal(err)
}
if _, err := fieldWriter.Write([]byte(content)); err != nil {
t.Fatal(err)
}

err = writer.Close()
if err != nil {
t.Fatal(err)
}

req, err := http.NewRequest("POST", server.URL()+"/other-bucket", &buf)
if err != nil {
t.Fatal(err)
}

req.Header.Set("Content-Type", writer.FormDataContentType())
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
expectedStatus := http.StatusNoContent
if resp.StatusCode != expectedStatus {
t.Errorf("wrong status code\nwant %d\ngot %d", expectedStatus, resp.StatusCode)
}

obj, err := server.GetObject("other-bucket", "object.txt")
if err != nil {
t.Fatal(err)
}
if string(obj.Content) != content {
t.Errorf("wrong content\nwant %q\ngot %q", string(obj.Content), content)
}
if obj.ContentType != contentType {
t.Errorf("wrong content type\nwant %q\ngot %q", contentType, obj.ContentType)
}
if want := map[string]string{"key": "Value"}; !reflect.DeepEqual(obj.Metadata, want) {
t.Errorf("wrong metadata\nwant %q\ngot %q", want, obj.Metadata)
}
checkChecksum(t, []byte(content), obj)
}

func isACLPublic(acl []storage.ACLRule) bool {
for _, entry := range acl {
if entry.Entity == storage.AllUsers && entry.Role == storage.RoleReader {
Expand Down
55 changes: 55 additions & 0 deletions fakestorage/xml_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package fakestorage

import (
"encoding/xml"
"net/http"
)

type xmlResponse struct {
status int
header http.Header
data interface{}
errorMessage string
}

type xmlHandler = func(r *http.Request) xmlResponse

func xmlToHTTPHandler(h xmlHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
resp := h(r)
w.Header().Set("Content-Type", "application/xml")
for name, values := range resp.header {
for _, value := range values {
w.Header().Add(name, value)
}
}

status := resp.getStatus()
var data interface{}
if status > 399 {
data = newErrorResponse(status, resp.getErrorMessage(status), nil)
} else {
data = resp.data
}

w.WriteHeader(status)
xml.NewEncoder(w).Encode(data)
}
}

func (r *xmlResponse) getStatus() int {
if r.status > 0 {
return r.status
}
if r.errorMessage != "" {
return http.StatusInternalServerError
}
return http.StatusOK
}

func (r *xmlResponse) getErrorMessage(status int) string {
if r.errorMessage != "" {
return r.errorMessage
}
return http.StatusText(status)
}

0 comments on commit 3aa1429

Please sign in to comment.