Repository: fsouza/fake-gcs-server
Pull request #521 (2021-06-29 02:48:27)

Add form data upload
Closes https://github.com/fsouza/fake-gcs-server/issues/270

This adds the form data upload method that is commonly used for websites. It does the basic, but could be extended to support further use cases down the line. I tried to follow the current code architecture as much as I could.


Patch:
diff --git a/examples/go/go.mod b/examples/go/go.mod
index 2cd2201..54df865 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -3,6 +3,6 @@ module github.com/zigius/fake-gcs-server/examples/go-example
 go 1.14
 
 require (
-	cloud.google.com/go/storage v1.15.0
-	google.golang.org/api v0.49.0
+	cloud.google.com/go/storage v1.16.0
+	google.golang.org/api v0.50.0
 )
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 4ccc356..bdc5480 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.15.0 h1:Ljj+ZXVEhCr/1+4ZhvtteN1ND7UUsNTlduGclLh8GO0=
-cloud.google.com/go/storage v1.15.0/go.mod h1:mjjQMoxxyGH7Jr8K5qrx6N2O0AHsczI61sMNn03GIZI=
+cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
+cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -253,10 +253,10 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 h1:x622Z2o4hgCr/4CiKWc51jHVKaWdtVpBNmEI8wI9Qns=
 golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -304,7 +304,6 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -372,8 +371,9 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -400,11 +400,11 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA=
 google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
 google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.49.0 h1:gjIBDxlTG7vnzMmEnYwTnvLTF8Rjzo+ETCgEX1YZ/fY=
 google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
+google.golang.org/api v0.50.0 h1:LX7NFCFYOHzr7WHaYiRUpeipZe9o5L8T+2F4Z798VDw=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -452,14 +452,14 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210420162539-3c870d7478d2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced h1:c5geK1iMU3cDKtFrCVQIcjR3W+JOZMuhIyICMCTbtus=
 google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84 h1:R1r5J0u6Cx+RNl/6mezTw6oA14cmKC96FeUwL6A9bd4=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt
index d3ea19c..f17617c 100644
--- a/examples/python/requirements.txt
+++ b/examples/python/requirements.txt
@@ -1,2 +1,2 @@
-google-cloud-storage==1.39.0
+google-cloud-storage==1.40.0
 urllib3==1.26.6
diff --git a/fakestorage/object.go b/fakestorage/object.go
index e023fb9..7fc548c 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -232,10 +232,28 @@ func (s *Server) createObject(obj Object) (Object, error) {
 	return fromBackendObjects([]backend.Object{newObj})[0], nil
 }
 
+type ListOptions struct {
+	Prefix      string
+	Delimiter   string
+	Versions    bool
+	StartOffset string
+	EndOffset   string
+}
+
 // ListObjects returns a sorted list of objects that match the given criteria,
 // or an error if the bucket doesn't exist.
+//
+// Deprecated: use ListObjectsWithOptions.
 func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool) ([]Object, []string, error) {
-	backendObjects, err := s.backend.ListObjects(bucketName, versions)
+	return s.ListObjectsWithOptions(bucketName, ListOptions{
+		Prefix:    prefix,
+		Delimiter: delimiter,
+		Versions:  versions,
+	})
+}
+
+func (s *Server) ListObjectsWithOptions(bucketName string, options ListOptions) ([]Object, []string, error) {
+	backendObjects, err := s.backend.ListObjects(bucketName, options.Versions)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -245,13 +263,18 @@ func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool
 	var respObjects []Object
 	prefixes := make(map[string]bool)
 	for _, obj := range olist {
-		if strings.HasPrefix(obj.Name, prefix) {
-			objName := strings.Replace(obj.Name, prefix, "", 1)
-			delimPos := strings.Index(objName, delimiter)
-			if delimiter != "" && delimPos > -1 {
-				prefixes[obj.Name[:len(prefix)+delimPos+1]] = true
+		if strings.HasPrefix(obj.Name, options.Prefix) {
+			objName := strings.Replace(obj.Name, options.Prefix, "", 1)
+			delimPos := strings.Index(objName, options.Delimiter)
+			if options.Delimiter != "" && delimPos > -1 {
+				prefix := obj.Name[:len(options.Prefix)+delimPos+1]
+				if isInOffset(prefix, options.StartOffset, options.EndOffset) {
+					prefixes[prefix] = true
+				}
 			} else {
-				respObjects = append(respObjects, obj)
+				if isInOffset(obj.Name, options.StartOffset, options.EndOffset) {
+					respObjects = append(respObjects, obj)
+				}
 			}
 		}
 	}
@@ -263,6 +286,18 @@ func (s *Server) ListObjects(bucketName, prefix, delimiter string, versions bool
 	return respObjects, respPrefixes, nil
 }
 
+func isInOffset(name, startOffset, endOffset string) bool {
+	if endOffset != "" && startOffset != "" {
+		return strings.Compare(name, endOffset) < 0 && strings.Compare(name, startOffset) >= 0
+	} else if endOffset != "" {
+		return strings.Compare(name, endOffset) < 0
+	} else if startOffset != "" {
+		return strings.Compare(name, startOffset) >= 0
+	} else {
+		return true
+	}
+}
+
 func getCurrentIfZero(date time.Time) time.Time {
 	if date.IsZero() {
 		return time.Now()
@@ -355,11 +390,13 @@ func (s *Server) objectWithGenerationOnValidGeneration(bucketName, objectName, g
 
 func (s *Server) listObjects(r *http.Request) jsonResponse {
 	bucketName := mux.Vars(r)["bucketName"]
-	prefix := r.URL.Query().Get("prefix")
-	delimiter := r.URL.Query().Get("delimiter")
-	versions := r.URL.Query().Get("versions")
-
-	objs, prefixes, err := s.ListObjects(bucketName, prefix, delimiter, versions == "true")
+	objs, prefixes, err := s.ListObjectsWithOptions(bucketName, ListOptions{
+		Prefix:      r.URL.Query().Get("prefix"),
+		Delimiter:   r.URL.Query().Get("delimiter"),
+		Versions:    r.URL.Query().Get("versions") == "true",
+		StartOffset: r.URL.Query().Get("startOffset"),
+		EndOffset:   r.URL.Query().Get("endOffset"),
+	})
 
 	if err != nil {
 		return jsonResponse{status: http.StatusNotFound}
diff --git a/fakestorage/server.go b/fakestorage/server.go
index 15e5567..6f8153e 100644
--- a/fakestorage/server.go
+++ b/fakestorage/server.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"mime"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -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))
@@ -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"
+}
diff --git a/fakestorage/upload.go b/fakestorage/upload.go
index 41da8d4..511f856 100644
--- a/fakestorage/upload.go
+++ b/fakestorage/upload.go
@@ -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")
 
diff --git a/fakestorage/xml_response.go b/fakestorage/xml_response.go
new file mode 100644
index 0000000..c6b9785
--- /dev/null
+++ b/fakestorage/xml_response.go
@@ -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)
+}
diff --git a/go.mod b/go.mod
index efc33a1..06cd371 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
 module github.com/fsouza/fake-gcs-server
 
 require (
-	cloud.google.com/go/storage v1.15.0
+	cloud.google.com/go/storage v1.16.0
 	github.com/google/go-cmp v0.5.6
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
 	github.com/sirupsen/logrus v1.8.1
-	google.golang.org/api v0.49.0
+	google.golang.org/api v0.50.0
 )
 
 go 1.15
diff --git a/go.sum b/go.sum
index d9bb69f..d1efa58 100644
--- a/go.sum
+++ b/go.sum
@@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.15.0 h1:Ljj+ZXVEhCr/1+4ZhvtteN1ND7UUsNTlduGclLh8GO0=
-cloud.google.com/go/storage v1.15.0/go.mod h1:mjjQMoxxyGH7Jr8K5qrx6N2O0AHsczI61sMNn03GIZI=
+cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
+cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -268,10 +268,10 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 h1:x622Z2o4hgCr/4CiKWc51jHVKaWdtVpBNmEI8wI9Qns=
 golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -320,7 +320,6 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -388,8 +387,9 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -416,11 +416,11 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA=
 google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
 google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.49.0 h1:gjIBDxlTG7vnzMmEnYwTnvLTF8Rjzo+ETCgEX1YZ/fY=
 google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
+google.golang.org/api v0.50.0 h1:LX7NFCFYOHzr7WHaYiRUpeipZe9o5L8T+2F4Z798VDw=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -468,14 +468,14 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210420162539-3c870d7478d2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced h1:c5geK1iMU3cDKtFrCVQIcjR3W+JOZMuhIyICMCTbtus=
 google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84 h1:R1r5J0u6Cx+RNl/6mezTw6oA14cmKC96FeUwL6A9bd4=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index 10d9a4d..8300c93 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -628,6 +628,43 @@ func getTestCasesForListTests(versioningEnabled, withOverwrites bool) []listTest
 			[]string{},
 			nil,
 		},
+		{
+			fmt.Sprintf("filtering endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{EndOffset: "img/low-res"},
+			[]string{
+				"img/brand.jpg",
+				"img/hi-res/party-01.jpg",
+				"img/hi-res/party-02.jpg",
+				"img/hi-res/party-03.jpg",
+			},
+			nil,
+		},
+		{
+			fmt.Sprintf("filtering startOffset and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{StartOffset: "img/hi-res", EndOffset: "img/low-res"},
+			[]string{
+				"img/hi-res/party-01.jpg",
+				"img/hi-res/party-02.jpg",
+				"img/hi-res/party-03.jpg",
+			},
+			nil,
+		},
+		{
+			fmt.Sprintf("filtering prefix, delimiter, startOffset and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{Prefix: "img/", Delimiter: "/", StartOffset: "img/hi-res", EndOffset: "img/low-res"},
+			[]string{},
+			[]string{"img/hi-res/"},
+		},
+		{
+			fmt.Sprintf("filtering prefix, delimiter and endOffset, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
+			"some-bucket",
+			&storage.Query{Prefix: "img/", Delimiter: "/", StartOffset: "", EndOffset: "img/low-res"},
+			[]string{"img/brand.jpg"},
+			[]string{"img/hi-res/"},
+		},
 	}
 }
 
diff --git a/fakestorage/upload_test.go b/fakestorage/upload_test.go
index 11553f6..c431d2e 100644
--- a/fakestorage/upload_test.go
+++ b/fakestorage/upload_test.go
@@ -13,6 +13,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"mime/multipart"
 	"net/http"
 	"reflect"
 	"strings"
@@ -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 {