getObject: Add support for special response headers.

Supports now response-content-type, response-content-disposition,
response-cache-control, response-expires.
master
Harshavardhana 9 years ago
parent de79440de2
commit 99fbc0fcb3
  1. 26
      object-handlers.go
  2. 8
      pkg/fs/signature.go
  3. 2
      vendor/github.com/minio/minio-go/CONTRIBUTING.md
  4. 2
      vendor/github.com/minio/minio-go/README.md
  5. 56
      vendor/github.com/minio/minio-go/api-presigned.go
  6. 2
      vendor/github.com/minio/minio-go/api-put-object-common.go
  7. 8
      vendor/github.com/minio/minio-go/api.go
  8. 34
      vendor/github.com/minio/minio-go/api_functional_v2_test.go
  9. 31
      vendor/github.com/minio/minio-go/api_functional_v4_test.go
  10. 17
      vendor/github.com/minio/minio-go/request-signature-v2.go
  11. 4
      vendor/vendor.json
  12. 7
      web-handlers.go

@ -18,6 +18,7 @@ package main
import ( import (
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -29,6 +30,24 @@ const (
maxPartsList = 1000 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 // GetObjectHandler - GET Object
// ---------- // ----------
// This implementation of the GET operation retrieves object. To use GET, // 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) writeErrorResponse(w, req, InvalidRange, req.URL.Path)
return return
} }
// Set standard object headers.
setObjectHeaders(w, metadata, hrange) 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 { if _, err = api.Filesystem.GetObject(w, bucket, object, hrange.start, hrange.length); err != nil {
errorIf(err.Trace(), "GetObject failed.", nil) errorIf(err.Trace(), "GetObject failed.", nil)
return return

@ -308,6 +308,14 @@ func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) {
query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds)) query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
query.Set("X-Amz-SignedHeaders", r.getSignedHeaders(r.extractSignedHeaders())) query.Set("X-Amz-SignedHeaders", r.getSignedHeaders(r.extractSignedHeaders()))
query.Set("X-Amz-Credential", r.AccessKeyID+"/"+r.getScope(t)) 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() encodedQuery := query.Encode()
// Verify if date query is same. // Verify if date query is same.

@ -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. - 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` - Run `go fmt`
- Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request. - 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 * Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project
- `minio-go` project is strictly conformant with Golang style - `minio-go` project is strictly conformant with Golang style

@ -83,7 +83,7 @@ func main() {
* [FGetObject(bucketName, objectName, filePath) error](examples/s3/fgetobject.go) * [FGetObject(bucketName, objectName, filePath) error](examples/s3/fgetobject.go)
### Presigned Operations. ### 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) * [PresignedPutObject(bucketName, objectName, time.Duration) (string, error)](examples/s3/presignedputobject.go)
* [PresignedPostPolicy(NewPostPolicy()) (map[string]string, error)](examples/s3/presignedpostpolicy.go) * [PresignedPostPolicy(NewPostPolicy()) (map[string]string, error)](examples/s3/presignedpostpolicy.go)

@ -18,13 +18,26 @@ package minio
import ( import (
"errors" "errors"
"net/url"
"time" "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'. // presignURL - Returns a presigned URL for an input 'method'.
// Expires maximum is 7days - ie. 604800 and minimum is 1. // 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. // Input validation.
if method == "" {
return "", ErrInvalidArgument("method cannot be empty.")
}
if err := isValidBucketName(bucketName); err != nil { if err := isValidBucketName(bucketName); err != nil {
return "", err return "", err
} }
@ -35,35 +48,50 @@ func (c Client) presignURL(method string, bucketName string, objectName string,
return "", err return "", err
} }
if method == "" { // Convert expires into seconds.
return "", ErrInvalidArgument("method cannot be empty.")
}
expireSeconds := int64(expires / time.Second) expireSeconds := int64(expires / time.Second)
// Instantiate a new request. reqMetadata := requestMetadata{
// Since expires is set newRequest will presign the request.
req, err := c.newRequest(method, requestMetadata{
presignURL: true, presignURL: true,
bucketName: bucketName, bucketName: bucketName,
objectName: objectName, objectName: objectName,
expires: expireSeconds, 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 { if err != nil {
return "", err return "", err
} }
return req.URL.String(), nil return req.URL.String(), nil
} }
// PresignedGetObject - Returns a presigned URL to access an object without credentials. // PresignedGetObject - Returns a presigned URL to access an object
// Expires maximum is 7days - ie. 604800 and minimum is 1. // without credentials. Expires maximum is 7days - ie. 604800 and
func (c Client) PresignedGetObject(bucketName string, objectName string, expires time.Duration) (url string, err error) { // minimum is 1. Additionally you can override a set of response
return c.presignURL("GET", bucketName, objectName, expires) // 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. // PresignedPutObject - Returns a presigned URL to upload an object without credentials.
// Expires maximum is 7days - ie. 604800 and minimum is 1. // 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) { 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. // PresignedPostPolicy - Returns POST form data to upload an object at a location.

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

@ -325,6 +325,12 @@ func (c Client) do(req *http.Request) (*http.Response, error) {
// execute the request. // execute the request.
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { 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 return resp, err
} }
// If trace is enabled, dump http request and response. // 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) PutObjectWithProgress(bucketName, objectName string, reader io.Reader, contentType string, progress io.Reader) (n int64, err error)
// Presigned operations. // 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) PresignedPutObject(bucketName, objectName string, expires time.Duration) (presignedURL string, err error)
PresignedPostPolicy(*PostPolicy) (formData map[string]string, err error) PresignedPostPolicy(*PostPolicy) (formData map[string]string, err error)

@ -24,6 +24,7 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"os" "os"
"testing" "testing"
"time" "time"
@ -954,11 +955,12 @@ func TestFunctionalV2(t *testing.T) {
t.Fatal("Error: ", err) 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 { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)
} }
// Verify if presigned url works.
resp, err := http.Get(presignedGetURL) resp, err := http.Get(presignedGetURL)
if err != nil { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)
@ -974,6 +976,34 @@ func TestFunctionalV2(t *testing.T) {
t.Fatal("Error: bytes mismatch.") 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) presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second)
if err != nil { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)

@ -24,6 +24,7 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"os" "os"
"testing" "testing"
"time" "time"
@ -983,11 +984,13 @@ func TestFunctional(t *testing.T) {
t.Fatal("Error: ", err) 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 { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)
} }
// Verify if presigned url works.
resp, err := http.Get(presignedGetURL) resp, err := http.Get(presignedGetURL)
if err != nil { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)
@ -1003,6 +1006,32 @@ func TestFunctional(t *testing.T) {
t.Fatal("Error: bytes mismatch.") 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) presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second)
if err != nil { if err != nil {
t.Fatal("Error: ", err) t.Fatal("Error: ", err)

@ -73,6 +73,11 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in
// Get encoded URL path. // Get encoded URL path.
path := encodeURL2Path(req.URL) 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. // Find epoch expires when the request will expire.
epochExpires := d.Unix() + expires epochExpires := d.Unix() + expires
@ -93,12 +98,16 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in
query.Set("AWSAccessKeyId", accessKeyID) 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("Expires", strconv.FormatInt(epochExpires, 10))
query.Set("Signature", signature)
// Encode query and save. // 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 return &req
} }
@ -283,7 +292,7 @@ func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request) {
// Request parameters // Request parameters
if len(vv[0]) > 0 { if len(vv[0]) > 0 {
buf.WriteByte('=') buf.WriteByte('=')
buf.WriteString(url.QueryEscape(vv[0])) buf.WriteString(strings.Replace(url.QueryEscape(vv[0]), "+", "%20", -1))
} }
} }
} }

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

@ -20,7 +20,9 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -206,7 +208,10 @@ func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply *
if e != nil { if e != nil {
return e 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 { if e != nil {
return e return e
} }

Loading…
Cancel
Save