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 (
"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

@ -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.

@ -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

@ -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)

@ -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.

@ -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

@ -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)

@ -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)

@ -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)

@ -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))
}
}
}

@ -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",

@ -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
}

Loading…
Cancel
Save