From 51fa4f7fe3d952cd83c96bd6b115b3f6ffc86734 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 20 Jan 2017 16:33:01 -0800 Subject: [PATCH] Make PutObject a nop for an object which ends with "/" and size is '0' (#3603) This helps majority of S3 compatible applications while not returning an error upon directory create request. Fixes #2965 --- cmd/bucket-notification-handlers.go | 12 +++----- cmd/bucket-policy-handlers.go | 21 ++++++------- cmd/fs-v1.go | 6 ++++ cmd/object-api-common.go | 27 +++++++++++++++++ cmd/object-handlers.go | 2 +- cmd/server_test.go | 47 ++++++++++++++++++++++++++++- cmd/xl-v1-object.go | 6 ++++ 7 files changed, 100 insertions(+), 21 deletions(-) diff --git a/cmd/bucket-notification-handlers.go b/cmd/bucket-notification-handlers.go index fb9c026ef..ad4a6333c 100644 --- a/cmd/bucket-notification-handlers.go +++ b/cmd/bucket-notification-handlers.go @@ -115,13 +115,11 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, return } - // If Content-Length is unknown or zero, deny the request. PutBucketNotification - // always needs a Content-Length if incoming request is not chunked. - if !contains(r.TransferEncoding, "chunked") { - if r.ContentLength == -1 { - writeErrorResponse(w, ErrMissingContentLength, r.URL) - return - } + // If Content-Length is unknown or zero, deny the request. + // PutBucketNotification always needs a Content-Length. + if r.ContentLength == -1 || r.ContentLength == 0 { + writeErrorResponse(w, ErrMissingContentLength, r.URL) + return } // Reads the incoming notification configuration. diff --git a/cmd/bucket-policy-handlers.go b/cmd/bucket-policy-handlers.go index 32dd0aac4..7a525dd95 100644 --- a/cmd/bucket-policy-handlers.go +++ b/cmd/bucket-policy-handlers.go @@ -142,18 +142,15 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht } // If Content-Length is unknown or zero, deny the - // request. PutBucketPolicy always needs a Content-Length if - // incoming request is not chunked. - if !contains(r.TransferEncoding, "chunked") { - if r.ContentLength == -1 || r.ContentLength == 0 { - writeErrorResponse(w, ErrMissingContentLength, r.URL) - return - } - // If Content-Length is greater than maximum allowed policy size. - if r.ContentLength > maxAccessPolicySize { - writeErrorResponse(w, ErrEntityTooLarge, r.URL) - return - } + // request. PutBucketPolicy always needs a Content-Length. + if r.ContentLength == -1 || r.ContentLength == 0 { + writeErrorResponse(w, ErrMissingContentLength, r.URL) + return + } + // If Content-Length is greater than maximum allowed policy size. + if r.ContentLength > maxAccessPolicySize { + writeErrorResponse(w, ErrEntityTooLarge, r.URL) + return } // Read access policy up to maxAccessPolicySize. diff --git a/cmd/fs-v1.go b/cmd/fs-v1.go index 6a486757a..6db9c5635 100644 --- a/cmd/fs-v1.go +++ b/cmd/fs-v1.go @@ -542,6 +542,12 @@ func (fs fsObjects) GetObjectInfo(bucket, object string) (ObjectInfo, error) { // Additionally writes `fs.json` which carries the necessary metadata // for future object operations. func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (objInfo ObjectInfo, err error) { + // This is a special case with size as '0' and object ends with + // a slash separator, we treat it like a valid operation and + // return success. + if isObjectDir(object, size) { + return dirObjectInfo(bucket, object, size, metadata), nil + } if err = checkPutObjectArgs(bucket, object, fs); err != nil { return ObjectInfo{}, err } diff --git a/cmd/object-api-common.go b/cmd/object-api-common.go index a371e3553..0f0c4a414 100644 --- a/cmd/object-api-common.go +++ b/cmd/object-api-common.go @@ -22,6 +22,7 @@ import ( "runtime" "strings" "sync" + "time" humanize "github.com/dustin/go-humanize" ) @@ -54,6 +55,32 @@ func isRemoteDisk(disk StorageAPI) bool { return ok } +// Checks if the object is a directory, this logic uses +// if size == 0 and object ends with slashSeparator then +// returns true. +func isObjectDir(object string, size int64) bool { + return strings.HasSuffix(object, slashSeparator) && size == 0 +} + +// Converts just bucket, object metadata into ObjectInfo datatype. +func dirObjectInfo(bucket, object string, size int64, metadata map[string]string) ObjectInfo { + // This is a special case with size as '0' and object ends with + // a slash separator, we treat it like a valid operation and + // return success. + md5Sum := metadata["md5Sum"] + delete(metadata, "md5Sum") + return ObjectInfo{ + Bucket: bucket, + Name: object, + ModTime: time.Now().UTC(), + ContentType: "application/octet-stream", + IsDir: true, + Size: size, + MD5Sum: md5Sum, + UserDefined: metadata, + } +} + // House keeping code for FS/XL and distributed Minio setup. func houseKeeping(storageDisks []StorageAPI) error { var wg = &sync.WaitGroup{} diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 3c9100ec7..73b10b32e 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -409,7 +409,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req return } } - if size == -1 && !contains(r.TransferEncoding, "chunked") { + if size == -1 { writeErrorResponse(w, ErrMissingContentLength, r.URL) return } diff --git a/cmd/server_test.go b/cmd/server_test.go index 9709f3ba6..edfdcafa0 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -130,6 +130,49 @@ func (s *TestSuiteCommon) TestBucketSQSNotificationWebHook(c *C) { verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest) } +func (s *TestSuiteCommon) TestObjectDir(c *C) { + bucketName := getRandomBucketName() + // HTTP request to create the bucket. + request, err := newTestSignedRequest("PUT", getMakeBucketURL(s.endPoint, bucketName), + 0, nil, s.accessKey, s.secretKey, s.signer) + c.Assert(err, IsNil) + + client := http.Client{Transport: s.transport} + // execute the request. + response, err := client.Do(request) + c.Assert(err, IsNil) + + // assert the http response status code. + c.Assert(response.StatusCode, Equals, http.StatusOK) + + request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, "my-object-directory/"), + 0, nil, s.accessKey, s.secretKey, s.signer) + c.Assert(err, IsNil) + + client = http.Client{Transport: s.transport} + // execute the HTTP request. + response, err = client.Do(request) + + c.Assert(err, IsNil) + // assert the http response status code. + c.Assert(response.StatusCode, Equals, http.StatusOK) + + request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, "my-object-directory/"), + 0, nil, s.accessKey, s.secretKey, s.signer) + c.Assert(err, IsNil) + + helloReader := bytes.NewReader([]byte("Hello, World")) + request.ContentLength = helloReader.Size() + request.Body = ioutil.NopCloser(helloReader) + + client = http.Client{Transport: s.transport} + // execute the HTTP request. + response, err = client.Do(request) + + c.Assert(err, IsNil) + verifyError(c, response, "XMinioInvalidObjectName", "Object name contains unsupported characters. Unsupported characters are `^*|\\\"", http.StatusBadRequest) +} + func (s *TestSuiteCommon) TestBucketSQSNotificationAMQP(c *C) { // Sample bucket notification. bucketNotificationBuf := `s3:ObjectCreated:Putprefiximages/1arn:minio:sqs:us-east-1:444455556666:amqp` @@ -1135,7 +1178,9 @@ func (s *TestSuiteCommon) TestSHA256Mismatch(c *C) { c.Assert(request.Header.Get("x-amz-content-sha256"), Equals, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") } // Set the body to generate signature mismatch. - request.Body = ioutil.NopCloser(bytes.NewReader([]byte("Hello, World"))) + helloReader := bytes.NewReader([]byte("Hello, World")) + request.ContentLength = helloReader.Size() + request.Body = ioutil.NopCloser(helloReader) c.Assert(err, IsNil) // execute the HTTP request. response, err = client.Do(request) diff --git a/cmd/xl-v1-object.go b/cmd/xl-v1-object.go index 8fa41ca77..2e4c632ca 100644 --- a/cmd/xl-v1-object.go +++ b/cmd/xl-v1-object.go @@ -440,6 +440,12 @@ func renameObject(disks []StorageAPI, srcBucket, srcObject, dstBucket, dstObject // writes `xl.json` which carries the necessary metadata for future // object operations. func (xl xlObjects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (objInfo ObjectInfo, err error) { + // This is a special case with size as '0' and object ends with + // a slash separator, we treat it like a valid operation and + // return success. + if isObjectDir(object, size) { + return dirObjectInfo(bucket, object, size, metadata), nil + } if err = checkPutObjectArgs(bucket, object, xl); err != nil { return ObjectInfo{}, err }