Skip to content

Commit

Permalink
Support XML API (fsouza#331)
Browse files Browse the repository at this point in the history
  • Loading branch information
tustvold committed May 12, 2023
1 parent d7832ec commit 0a1124f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 27 deletions.
68 changes: 68 additions & 0 deletions fakestorage/object.go
Expand Up @@ -13,6 +13,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -626,6 +627,72 @@ func (s *Server) xmlListObjects(r *http.Request) xmlResponse {
}
}

func (s *Server) xmlPutObject(r *http.Request) xmlResponse {
// https://cloud.google.com/storage/docs/xml-api/put-object-upload
vars := unescapeMuxVars(mux.Vars(r))
defer r.Body.Close()

metaData := make(map[string]string)
for key := range r.Header {
lowerKey := strings.ToLower(key)
if metaDataKey := strings.TrimPrefix(lowerKey, "x-goog-meta-"); metaDataKey != lowerKey {
metaData[metaDataKey] = r.Header.Get(key)
}
}

obj := StreamingObject{
ObjectAttrs: ObjectAttrs{
BucketName: vars["bucketName"],
Name: vars["objectName"],
ContentType: r.Header.Get(contentTypeHeader),
ContentEncoding: r.Header.Get(contentEncodingHeader),
Metadata: metaData,
},
}
if source := r.Header.Get("x-goog-copy-source"); source != "" {
escaped, err := url.PathUnescape(source)
if err != nil {
return xmlResponse{status: http.StatusBadRequest}
}

split := strings.SplitN(escaped, "/", 2)
if len(split) != 2 {
return xmlResponse{status: http.StatusBadRequest}
}

sourceObject, err := s.GetObjectStreaming(split[0], split[1])
if err != nil {
return xmlResponse{status: http.StatusNotFound}
}
obj.Content = sourceObject.Content
} else {
obj.Content = notImplementedSeeker{r.Body}
}

obj, err := s.createObject(obj, backend.NoConditions{})

if err != nil {
return xmlResponse{
status: http.StatusInternalServerError,
errorMessage: err.Error(),
}
}

obj.Close()
return xmlResponse{
status: http.StatusOK,
}
}

func (s *Server) xmlDeleteObject(r *http.Request) xmlResponse {
resp := s.deleteObject(r)
return xmlResponse{
status: resp.status,
errorMessage: resp.errorMessage,
header: resp.header,
}
}

func (s *Server) getObject(w http.ResponseWriter, r *http.Request) {
if alt := r.URL.Query().Get("alt"); alt == "media" || r.Method == http.MethodHead {
s.downloadObject(w, r)
Expand Down Expand Up @@ -867,6 +934,7 @@ func (s *Server) downloadObject(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Goog-Generation", strconv.FormatInt(obj.Generation, 10))
w.Header().Set("X-Goog-Hash", fmt.Sprintf("crc32c=%s,md5=%s", obj.Crc32c, obj.Md5Hash))
w.Header().Set("Last-Modified", obj.Updated.Format(http.TimeFormat))
w.Header().Set("ETag", obj.Etag)
for name, value := range obj.Metadata {
w.Header().Set("X-Goog-Meta-"+name, value)
}
Expand Down
9 changes: 3 additions & 6 deletions fakestorage/server.go
Expand Up @@ -276,6 +276,9 @@ func (s *Server) buildMuxer() {
for _, r := range xmlApiRouters {
r.Path("/").Methods(http.MethodGet).HandlerFunc(xmlToHTTPHandler(s.xmlListObjects))
r.Path("").Methods(http.MethodGet).HandlerFunc(xmlToHTTPHandler(s.xmlListObjects))
r.Path("/{objectName:.+}").Methods(http.MethodPut).HandlerFunc(xmlToHTTPHandler(s.xmlPutObject))
r.Path("/{objectName:.+}").Methods(http.MethodGet, http.MethodHead).HandlerFunc(s.downloadObject)
r.Path("/{objectName:.+}").Methods(http.MethodDelete).HandlerFunc(xmlToHTTPHandler(s.xmlDeleteObject))
}

bucketHost := fmt.Sprintf("{bucketName}.%s", s.publicHost)
Expand All @@ -296,12 +299,6 @@ func (s *Server) buildMuxer() {
handler.Host(s.publicHost).Path("/{bucketName}").MatcherFunc(matchFormData).Methods(http.MethodPost, http.MethodPut).HandlerFunc(xmlToHTTPHandler(s.insertFormObject))
handler.Host(bucketHost).MatcherFunc(matchFormData).Methods(http.MethodPost, http.MethodPut).HandlerFunc(xmlToHTTPHandler(s.insertFormObject))

// Signed URLs (upload and download)
handler.MatcherFunc(s.publicHostMatcher).Path("/{bucketName}/{objectName:.+}").Methods(http.MethodPost, http.MethodPut).HandlerFunc(jsonToHTTPHandler(s.insertObject))
handler.MatcherFunc(s.publicHostMatcher).Path("/{bucketName}/{objectName:.+}").Methods(http.MethodGet, http.MethodHead).HandlerFunc(s.getObject)
handler.Host(bucketHost).Path("/{objectName:.+}").Methods(http.MethodPost, http.MethodPut).HandlerFunc(jsonToHTTPHandler(s.insertObject))
handler.Host("{bucketName:.+}").Path("/{objectName:.+}").Methods(http.MethodPost, http.MethodPut).HandlerFunc(jsonToHTTPHandler(s.insertObject))

s.handler = handler
}

Expand Down
2 changes: 2 additions & 0 deletions fakestorage/upload.go
Expand Up @@ -27,6 +27,8 @@ import (

const contentTypeHeader = "Content-Type"

const contentEncodingHeader = "Content-Encoding"

const (
uploadTypeMedia = "media"
uploadTypeMultipart = "multipart"
Expand Down
79 changes: 58 additions & 21 deletions fakestorage/upload_test.go
Expand Up @@ -10,7 +10,6 @@ import (
"context"
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"mime/multipart"
Expand Down Expand Up @@ -570,9 +569,6 @@ func TestServerClientSignedUpload(t *testing.T) {

func TestServerClientSignedUploadBucketCNAME(t *testing.T) {
url := "https://mybucket.mydomain.com:4443/files/txt/text-02.txt?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=fake-gcs&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=fake-gc"
expectedName := "files/txt/text-02.txt"
expectedContentType := "text/plain"
expectedHash := "bHupxaFBQh4cA8uYB8l8dA=="
opts := Options{
InitialObjects: []Object{
{ObjectAttrs: ObjectAttrs{BucketName: "mybucket.mydomain.com", Name: "files/txt/text-01.txt"}, Content: []byte("something")},
Expand All @@ -596,23 +592,6 @@ func TestServerClientSignedUploadBucketCNAME(t *testing.T) {
if resp.StatusCode != http.StatusOK {
t.Errorf("wrong status returned\nwant %d\ngot %d", http.StatusOK, resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
var obj Object
if err := json.Unmarshal(data, &obj); err != nil {
t.Fatal(err)
}
if obj.Name != expectedName {
t.Errorf("wrong filename\nwant %q\ngot %q", expectedName, obj.Name)
}
if obj.ContentType != expectedContentType {
t.Errorf("wrong content type\nwant %q\ngot %q", expectedContentType, obj.ContentType)
}
if obj.Md5Hash != expectedHash {
t.Errorf("wrong md5 hash\nwant %q\ngot %q", expectedHash, obj.Md5Hash)
}
}

func TestServerClientUploadWithPredefinedAclPublicRead(t *testing.T) {
Expand Down Expand Up @@ -700,6 +679,64 @@ func TestServerClientSimpleUploadNoName(t *testing.T) {
}
}

func TestServerXMLPut(t *testing.T) {
server, err := NewServerWithOptions(Options{
PublicHost: "test",
})
if err != nil {
t.Fatal(err)
}
defer server.Stop()
server.CreateBucketWithOpts(CreateBucketOpts{Name: "bucket1"})
server.CreateBucketWithOpts(CreateBucketOpts{Name: "bucket2"})

const data = "some nice content"
req, err := http.NewRequest("PUT", server.URL()+"/bucket1/path", strings.NewReader(data))
req.Host = "test"
if err != nil {
t.Fatal(err)
}
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()
if resp.StatusCode != http.StatusOK {
t.Errorf("got %d expected %d", resp.StatusCode, http.StatusOK)
}

req, err = http.NewRequest("PUT", server.URL()+"/bucket2/path", nil)
req.Host = "test"
req.Header.Set("x-goog-copy-source", "bucket1/path")

resp, err = client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("got %d expected %d", resp.StatusCode, http.StatusOK)
}

req, err = http.NewRequest("PUT", server.URL()+"/bucket2/path2", nil)
req.Host = "test"
req.Header.Set("x-goog-copy-source", "bucket1/nonexistent")

resp, err = client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("got %d expected %d", resp.StatusCode, http.StatusNotFound)
}
}

func TestServerInvalidUploadType(t *testing.T) {
server := NewServer(nil)
defer server.Stop()
Expand Down

0 comments on commit 0a1124f

Please sign in to comment.