getObject: Add support for special response headers.

Supports now response-content-type, response-content-disposition,
response-cache-control, response-expires.
This commit is contained in:
Harshavardhana 2016-02-07 03:37:54 -08:00
parent de79440de2
commit 99fbc0fcb3
12 changed files with 169 additions and 28 deletions

View File

@ -18,6 +18,7 @@ package main
import (
"net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
@ -29,6 +30,24 @@ const (
maxPartsList = 1000
)
// supportedGetReqParams - supported request parameters for GET
// presigned request.
var supportedGetReqParams = map[string]string{
"response-expires": "Expires",
"response-content-type": "Content-Type",
"response-cache-control": "Cache-Control",
"response-content-disposition": "Content-Disposition",
}
// setResponseHeaders - set any requested parameters as response headers.
func setResponseHeaders(w http.ResponseWriter, reqParams url.Values) {
for k, v := range reqParams {
if header, ok := supportedGetReqParams[k]; ok {
w.Header()[header] = v
}
}
}
// GetObjectHandler - GET Object
// ----------
// This implementation of the GET operation retrieves object. To use GET,
@ -69,7 +88,14 @@ func (api CloudStorageAPI) GetObjectHandler(w http.ResponseWriter, req *http.Req
writeErrorResponse(w, req, InvalidRange, req.URL.Path)
return
}
// Set standard object headers.
setObjectHeaders(w, metadata, hrange)
// Set any additional requested response headers.
setResponseHeaders(w, req.URL.Query())
// Get the object.
if _, err = api.Filesystem.GetObject(w, bucket, object, hrange.start, hrange.length); err != nil {
errorIf(err.Trace(), "GetObject failed.", nil)
return

View File

@ -308,6 +308,14 @@ func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) {
query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
query.Set("X-Amz-SignedHeaders", r.getSignedHeaders(r.extractSignedHeaders()))
query.Set("X-Amz-Credential", r.AccessKeyID+"/"+r.getScope(t))
// Save other headers available in the request parameters.
for k, v := range r.Request.URL.Query() {
if strings.HasPrefix(strings.ToLower(k), "x-amz") {
continue
}
query[k] = v
}
encodedQuery := query.Encode()
// Verify if date query is same.

View File

@ -14,7 +14,7 @@
- Have test cases for the new code. If you have questions about how to do it, please ask in your pull request.
- Run `go fmt`
- Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
- Make sure `go test -race ./...` and `go build` completes.
- Make sure `go test -short -race ./...` and `go build` completes.
* Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project
- `minio-go` project is strictly conformant with Golang style

View File

@ -83,7 +83,7 @@ func main() {
* [FGetObject(bucketName, objectName, filePath) error](examples/s3/fgetobject.go)
### Presigned Operations.
* [PresignedGetObject(bucketName, objectName, time.Duration) (string, error)](examples/s3/presignedgetobject.go)
* [PresignedGetObject(bucketName, objectName, time.Duration, url.Values) (string, error)](examples/s3/presignedgetobject.go)
* [PresignedPutObject(bucketName, objectName, time.Duration) (string, error)](examples/s3/presignedputobject.go)
* [PresignedPostPolicy(NewPostPolicy()) (map[string]string, error)](examples/s3/presignedpostpolicy.go)

View File

@ -18,13 +18,26 @@ package minio
import (
"errors"
"net/url"
"time"
)
// supportedGetReqParams - supported request parameters for GET
// presigned request.
var supportedGetReqParams = map[string]struct{}{
"response-expires": struct{}{},
"response-content-type": struct{}{},
"response-cache-control": struct{}{},
"response-content-disposition": struct{}{},
}
// presignURL - Returns a presigned URL for an input 'method'.
// Expires maximum is 7days - ie. 604800 and minimum is 1.
func (c Client) presignURL(method string, bucketName string, objectName string, expires time.Duration) (url string, err error) {
func (c Client) presignURL(method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values) (urlStr string, err error) {
// Input validation.
if method == "" {
return "", ErrInvalidArgument("method cannot be empty.")
}
if err := isValidBucketName(bucketName); err != nil {
return "", err
}
@ -35,35 +48,50 @@ func (c Client) presignURL(method string, bucketName string, objectName string,
return "", err
}
if method == "" {
return "", ErrInvalidArgument("method cannot be empty.")
}
// Convert expires into seconds.
expireSeconds := int64(expires / time.Second)
// Instantiate a new request.
// Since expires is set newRequest will presign the request.
req, err := c.newRequest(method, requestMetadata{
reqMetadata := requestMetadata{
presignURL: true,
bucketName: bucketName,
objectName: objectName,
expires: expireSeconds,
})
}
// For "GET" we are handling additional request parameters to
// override its response headers.
if method == "GET" {
// Verify if input map has unsupported params, if yes exit.
for k := range reqParams {
if _, ok := supportedGetReqParams[k]; !ok {
return "", ErrInvalidArgument(k + " unsupported request parameter for presigned GET.")
}
}
// Save the request parameters to be used in presigning for
// GET request.
reqMetadata.queryValues = reqParams
}
// Instantiate a new request.
// Since expires is set newRequest will presign the request.
req, err := c.newRequest(method, reqMetadata)
if err != nil {
return "", err
}
return req.URL.String(), nil
}
// PresignedGetObject - Returns a presigned URL to access an object without credentials.
// Expires maximum is 7days - ie. 604800 and minimum is 1.
func (c Client) PresignedGetObject(bucketName string, objectName string, expires time.Duration) (url string, err error) {
return c.presignURL("GET", bucketName, objectName, expires)
// PresignedGetObject - Returns a presigned URL to access an object
// without credentials. Expires maximum is 7days - ie. 604800 and
// minimum is 1. Additionally you can override a set of response
// headers using the query parameters.
func (c Client) PresignedGetObject(bucketName string, objectName string, expires time.Duration, reqParams url.Values) (url string, err error) {
return c.presignURL("GET", bucketName, objectName, expires, reqParams)
}
// PresignedPutObject - Returns a presigned URL to upload an object without credentials.
// Expires maximum is 7days - ie. 604800 and minimum is 1.
func (c Client) PresignedPutObject(bucketName string, objectName string, expires time.Duration) (url string, err error) {
return c.presignURL("PUT", bucketName, objectName, expires)
return c.presignURL("PUT", bucketName, objectName, expires, nil)
}
// PresignedPostPolicy - Returns POST form data to upload an object at a location.

View File

@ -55,7 +55,7 @@ func shouldUploadPart(objPart objectPart, objectParts map[int]objectPart) bool {
return true
}
// if md5sum mismatches should upload the part.
if objPart.ETag == uploadedPart.ETag {
if objPart.ETag != uploadedPart.ETag {
return true
}
return false

View File

@ -325,6 +325,12 @@ func (c Client) do(req *http.Request) (*http.Response, error) {
// execute the request.
resp, err := c.httpClient.Do(req)
if err != nil {
// Handle this specifically for now until future Golang
// versions fix this issue properly.
urlErr, ok := err.(*url.Error)
if ok && strings.Contains(urlErr.Err.Error(), "EOF") {
return nil, fmt.Errorf("Connection closed by foreign host %s. Retry again.", urlErr.URL)
}
return resp, err
}
// If trace is enabled, dump http request and response.
@ -516,7 +522,7 @@ type CloudStorageClient interface {
PutObjectWithProgress(bucketName, objectName string, reader io.Reader, contentType string, progress io.Reader) (n int64, err error)
// Presigned operations.
PresignedGetObject(bucketName, objectName string, expires time.Duration) (presignedURL string, err error)
PresignedGetObject(bucketName, objectName string, expires time.Duration, reqParams url.Values) (presignedURL string, err error)
PresignedPutObject(bucketName, objectName string, expires time.Duration) (presignedURL string, err error)
PresignedPostPolicy(*PostPolicy) (formData map[string]string, err error)

View File

@ -24,6 +24,7 @@ import (
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"testing"
"time"
@ -954,11 +955,12 @@ func TestFunctionalV2(t *testing.T) {
t.Fatal("Error: ", err)
}
presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second)
// Generate presigned GET object url.
presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second, nil)
if err != nil {
t.Fatal("Error: ", err)
}
// Verify if presigned url works.
resp, err := http.Get(presignedGetURL)
if err != nil {
t.Fatal("Error: ", err)
@ -974,6 +976,34 @@ func TestFunctionalV2(t *testing.T) {
t.Fatal("Error: bytes mismatch.")
}
// Set request parameters.
reqParams := make(url.Values)
reqParams.Set("response-content-disposition", "attachment; filename=\"test.txt\"")
// Generate presigned GET object url.
presignedGetURL, err = c.PresignedGetObject(bucketName, objectName, 3600*time.Second, reqParams)
if err != nil {
t.Fatal("Error: ", err)
}
// Verify if presigned url works.
resp, err = http.Get(presignedGetURL)
if err != nil {
t.Fatal("Error: ", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Error: ", resp.Status)
}
newPresignedBytes, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal("Error: ", err)
}
if !bytes.Equal(newPresignedBytes, buf) {
t.Fatal("Error: bytes mismatch for presigned GET url.")
}
// Verify content disposition.
if resp.Header.Get("Content-Disposition") != "attachment; filename=\"test.txt\"" {
t.Fatalf("Error: wrong Content-Disposition received %s", resp.Header.Get("Content-Disposition"))
}
presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second)
if err != nil {
t.Fatal("Error: ", err)

View File

@ -24,6 +24,7 @@ import (
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"testing"
"time"
@ -983,11 +984,13 @@ func TestFunctional(t *testing.T) {
t.Fatal("Error: ", err)
}
presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second)
// Generate presigned GET object url.
presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second, nil)
if err != nil {
t.Fatal("Error: ", err)
}
// Verify if presigned url works.
resp, err := http.Get(presignedGetURL)
if err != nil {
t.Fatal("Error: ", err)
@ -1003,6 +1006,32 @@ func TestFunctional(t *testing.T) {
t.Fatal("Error: bytes mismatch.")
}
// Set request parameters.
reqParams := make(url.Values)
reqParams.Set("response-content-disposition", "attachment; filename=\"test.txt\"")
presignedGetURL, err = c.PresignedGetObject(bucketName, objectName, 3600*time.Second, reqParams)
if err != nil {
t.Fatal("Error: ", err)
}
// Verify if presigned url works.
resp, err = http.Get(presignedGetURL)
if err != nil {
t.Fatal("Error: ", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Error: ", resp.Status)
}
newPresignedBytes, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal("Error: ", err)
}
if !bytes.Equal(newPresignedBytes, buf) {
t.Fatal("Error: bytes mismatch for presigned GET URL.")
}
if resp.Header.Get("Content-Disposition") != "attachment; filename=\"test.txt\"" {
t.Fatalf("Error: wrong Content-Disposition received %s", resp.Header.Get("Content-Disposition"))
}
presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second)
if err != nil {
t.Fatal("Error: ", err)

View File

@ -73,6 +73,11 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in
// Get encoded URL path.
path := encodeURL2Path(req.URL)
if len(req.URL.Query()) > 0 {
// Keep the usual queries unescaped for string to sign.
query, _ := url.QueryUnescape(queryEncode(req.URL.Query()))
path = path + "?" + query
}
// Find epoch expires when the request will expire.
epochExpires := d.Unix() + expires
@ -93,12 +98,16 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in
query.Set("AWSAccessKeyId", accessKeyID)
}
// Fill in Expires and Signature for presigned query.
// Fill in Expires for presigned query.
query.Set("Expires", strconv.FormatInt(epochExpires, 10))
query.Set("Signature", signature)
// Encode query and save.
req.URL.RawQuery = query.Encode()
req.URL.RawQuery = queryEncode(query)
// Save signature finally.
req.URL.RawQuery += "&Signature=" + urlEncodePath(signature)
// Return.
return &req
}
@ -283,7 +292,7 @@ func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request) {
// Request parameters
if len(vv[0]) > 0 {
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(vv[0]))
buf.WriteString(strings.Replace(url.QueryEscape(vv[0]), "+", "%20", -1))
}
}
}

4
vendor/vendor.json vendored
View File

@ -59,8 +59,8 @@
},
{
"path": "github.com/minio/minio-go",
"revision": "c5884ce9ce3ac73b025d0bc58c4d3d72870edc0b",
"revisionTime": "2016-02-02T13:13:10+05:30"
"revision": "280f16a52008d3ebba1bd64398b9b082e6738386",
"revisionTime": "2016-02-07T03:45:25-08:00"
},
{
"path": "github.com/minio/minio-xl/pkg/atomic",

View File

@ -20,7 +20,9 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
@ -206,7 +208,10 @@ func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply *
if e != nil {
return e
}
signedURLStr, e := client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second)
reqParams := make(url.Values)
// Set content disposition for browser to download the file.
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(args.ObjectName)))
signedURLStr, e := client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second, reqParams)
if e != nil {
return e
}