mirror of
https://github.com/minio/minio.git
synced 2025-04-20 02:27:50 -04:00
fix: Add support for preserving mtime for replication (#9995)
This PR is needed for bucket replication support
This commit is contained in:
parent
6136a963c8
commit
2743d4ca87
@ -1419,7 +1419,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := "safe"
|
mode := "safemode"
|
||||||
if newObjectLayerFn() != nil {
|
if newObjectLayerFn() != nil {
|
||||||
mode = "online"
|
mode = "online"
|
||||||
}
|
}
|
||||||
|
@ -1909,6 +1909,12 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
// their internal error types. This code is only
|
// their internal error types. This code is only
|
||||||
// useful with gateway implementations.
|
// useful with gateway implementations.
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
|
case InvalidArgument:
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: e.Error(),
|
||||||
|
HTTPStatusCode: errorCodes[ErrInvalidRequest].HTTPStatusCode,
|
||||||
|
}
|
||||||
case *xml.SyntaxError:
|
case *xml.SyntaxError:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "MalformedXML",
|
Code: "MalformedXML",
|
||||||
|
@ -698,6 +698,10 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
||||||
|
if newObjectLayerFn() == nil {
|
||||||
|
// Server still in safe mode.
|
||||||
|
w.Header().Set(xhttp.MinIOServerStatus, "safemode")
|
||||||
|
}
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
if mType != mimeNone {
|
if mType != mimeNone {
|
||||||
w.Header().Set(xhttp.ContentType, string(mType))
|
w.Header().Set(xhttp.ContentType, string(mType))
|
||||||
@ -760,6 +764,10 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
|||||||
// The request is from browser and also if browser
|
// The request is from browser and also if browser
|
||||||
// is enabled we need to redirect.
|
// is enabled we need to redirect.
|
||||||
if browser && globalBrowserEnabled {
|
if browser && globalBrowserEnabled {
|
||||||
|
if newObjectLayerFn() == nil {
|
||||||
|
// server still in safe mode.
|
||||||
|
w.Header().Set(xhttp.MinIOServerStatus, "safemode")
|
||||||
|
}
|
||||||
w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path)
|
w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path)
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -918,28 +919,28 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
forceDelete := false
|
// Verify if the caller has sufficient permissions.
|
||||||
if value := r.Header.Get(xhttp.MinIOForceDelete); value != "" {
|
|
||||||
switch value {
|
|
||||||
case "true":
|
|
||||||
forceDelete = true
|
|
||||||
case "false":
|
|
||||||
default:
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if forceDelete {
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ForceDeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceDelete := false
|
||||||
|
if value := r.Header.Get(xhttp.MinIOForceDelete); value != "" {
|
||||||
|
var err error
|
||||||
|
forceDelete, err = strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||||
|
apiErr.Description = err.Error()
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if force delete header is set, we need to evaluate the policy anyways
|
||||||
|
// regardless of it being true or not.
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.ForceDeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if forceDelete {
|
if forceDelete {
|
||||||
@ -948,6 +949,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deleteBucket := objectAPI.DeleteBucket
|
deleteBucket := objectAPI.DeleteBucket
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2017, 2018 MinIO, Inc.
|
* MinIO Cloud Storage, (C) 2017-2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
@ -31,8 +30,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
@ -854,200 +851,3 @@ func deriveClientKey(clientKey [32]byte, bucket, object string) [32]byte {
|
|||||||
mac.Sum(key[:0])
|
mac.Sum(key[:0])
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
// set encryption options for pass through to backend in the case of gateway and UserDefined metadata
|
|
||||||
func getDefaultOpts(header http.Header, copySource bool, metadata map[string]string) (opts ObjectOptions, err error) {
|
|
||||||
var clientKey [32]byte
|
|
||||||
var sse encrypt.ServerSide
|
|
||||||
|
|
||||||
opts = ObjectOptions{UserDefined: metadata}
|
|
||||||
if copySource {
|
|
||||||
if crypto.SSECopy.IsRequested(header) {
|
|
||||||
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
opts.ServerSideEncryption = encrypt.SSECopy(sse)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if crypto.SSEC.IsRequested(header) {
|
|
||||||
clientKey, err = crypto.SSEC.ParseHTTP(header)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
opts.ServerSideEncryption = sse
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
|
||||||
opts.ServerSideEncryption = encrypt.NewSSE()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ObjectOptions for GET calls from encryption headers
|
|
||||||
func getOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
|
||||||
var (
|
|
||||||
encryption encrypt.ServerSide
|
|
||||||
opts ObjectOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
var partNumber int
|
|
||||||
var err error
|
|
||||||
if pn := r.URL.Query().Get("partNumber"); pn != "" {
|
|
||||||
partNumber, err = strconv.Atoi(pn)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
if partNumber <= 0 {
|
|
||||||
return opts, errInvalidArgument
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vid := strings.TrimSpace(r.URL.Query().Get("versionId"))
|
|
||||||
if vid != "" && vid != nullVersionID {
|
|
||||||
_, err := uuid.Parse(vid)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return opts, VersionNotFound{
|
|
||||||
Bucket: bucket,
|
|
||||||
Object: object,
|
|
||||||
VersionID: vid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
|
||||||
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
derivedKey := deriveClientKey(key, bucket, object)
|
|
||||||
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
|
||||||
logger.CriticalIf(ctx, err)
|
|
||||||
return ObjectOptions{
|
|
||||||
ServerSideEncryption: encryption,
|
|
||||||
VersionID: vid,
|
|
||||||
PartNumber: partNumber,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// default case of passing encryption headers to backend
|
|
||||||
opts, err = getDefaultOpts(r.Header, false, nil)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
opts.PartNumber = partNumber
|
|
||||||
opts.VersionID = vid
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
|
||||||
opts, err = getOpts(ctx, r, bucket, object)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
opts.Versioned = versioned
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ObjectOptions for PUT calls from encryption headers and metadata
|
|
||||||
func putOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
|
||||||
vid := strings.TrimSpace(r.URL.Query().Get("versionId"))
|
|
||||||
if vid != "" && vid != nullVersionID {
|
|
||||||
_, err := uuid.Parse(vid)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return opts, VersionNotFound{
|
|
||||||
Bucket: bucket,
|
|
||||||
Object: object,
|
|
||||||
VersionID: vid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the case of multipart custom format, the metadata needs to be checked in addition to header to see if it
|
|
||||||
// is SSE-S3 encrypted, primarily because S3 protocol does not require SSE-S3 headers in PutObjectPart calls
|
|
||||||
if GlobalGatewaySSE.SSES3() && (crypto.S3.IsRequested(r.Header) || crypto.S3.IsEncrypted(metadata)) {
|
|
||||||
return ObjectOptions{
|
|
||||||
ServerSideEncryption: encrypt.NewSSE(),
|
|
||||||
UserDefined: metadata,
|
|
||||||
VersionID: vid,
|
|
||||||
Versioned: versioned,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
|
||||||
opts, err = getOpts(ctx, r, bucket, object)
|
|
||||||
opts.VersionID = vid
|
|
||||||
opts.Versioned = versioned
|
|
||||||
opts.UserDefined = metadata
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if crypto.S3KMS.IsRequested(r.Header) {
|
|
||||||
keyID, context, err := crypto.S3KMS.ParseHTTP(r.Header)
|
|
||||||
if err != nil {
|
|
||||||
return ObjectOptions{}, err
|
|
||||||
}
|
|
||||||
sseKms, err := encrypt.NewSSEKMS(keyID, context)
|
|
||||||
if err != nil {
|
|
||||||
return ObjectOptions{}, err
|
|
||||||
}
|
|
||||||
return ObjectOptions{
|
|
||||||
ServerSideEncryption: sseKms,
|
|
||||||
UserDefined: metadata,
|
|
||||||
VersionID: vid,
|
|
||||||
Versioned: versioned,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
// default case of passing encryption headers and UserDefined metadata to backend
|
|
||||||
opts, err = getDefaultOpts(r.Header, false, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
opts.VersionID = vid
|
|
||||||
opts.Versioned = versioned
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ObjectOptions for Copy calls with encryption headers provided on the target side and source side metadata
|
|
||||||
func copyDstOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
|
||||||
return putOpts(ctx, r, bucket, object, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ObjectOptions for Copy calls with encryption headers provided on the source side
|
|
||||||
func copySrcOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
|
||||||
var (
|
|
||||||
ssec encrypt.ServerSide
|
|
||||||
opts ObjectOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
if GlobalGatewaySSE.SSEC() && crypto.SSECopy.IsRequested(r.Header) {
|
|
||||||
key, err := crypto.SSECopy.ParseHTTP(r.Header)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
derivedKey := deriveClientKey(key, bucket, object)
|
|
||||||
ssec, err = encrypt.NewSSEC(derivedKey[:])
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(ssec)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// default case of passing encryption headers to backend
|
|
||||||
opts, err := getDefaultOpts(r.Header, false, nil)
|
|
||||||
if err != nil {
|
|
||||||
return opts, err
|
|
||||||
}
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
@ -215,7 +215,11 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
|
|
||||||
fi.DataDir = mustGetUUID()
|
fi.DataDir = mustGetUUID()
|
||||||
fi.ModTime = UTCNow()
|
fi.ModTime = UTCNow()
|
||||||
|
if opts.UserDefined != nil {
|
||||||
fi.Metadata = opts.UserDefined
|
fi.Metadata = opts.UserDefined
|
||||||
|
} else {
|
||||||
|
fi.Metadata = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
uploadID := mustGetUUID()
|
uploadID := mustGetUUID()
|
||||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||||
@ -691,7 +695,10 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
|
|
||||||
// Save the final object size and modtime.
|
// Save the final object size and modtime.
|
||||||
fi.Size = objectSize
|
fi.Size = objectSize
|
||||||
|
fi.ModTime = opts.MTime
|
||||||
|
if opts.MTime.IsZero() {
|
||||||
fi.ModTime = UTCNow()
|
fi.ModTime = UTCNow()
|
||||||
|
}
|
||||||
|
|
||||||
// Save successfully calculated md5sum.
|
// Save successfully calculated md5sum.
|
||||||
fi.Metadata["etag"] = s3MD5
|
fi.Metadata["etag"] = s3MD5
|
||||||
|
@ -698,9 +698,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save additional erasureMetadata.
|
|
||||||
modTime := UTCNow()
|
|
||||||
|
|
||||||
opts.UserDefined["etag"] = r.MD5CurrentHexString()
|
opts.UserDefined["etag"] = r.MD5CurrentHexString()
|
||||||
|
|
||||||
// Guess content-type from the extension if possible.
|
// Guess content-type from the extension if possible.
|
||||||
@ -708,6 +705,11 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modTime := opts.MTime
|
||||||
|
if opts.MTime.IsZero() {
|
||||||
|
modTime = UTCNow()
|
||||||
|
}
|
||||||
|
|
||||||
// Fill all the necessary metadata.
|
// Fill all the necessary metadata.
|
||||||
// Update `xl.meta` content on each disks.
|
// Update `xl.meta` content on each disks.
|
||||||
for index := range partsMetadata {
|
for index := range partsMetadata {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
* MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -126,9 +126,28 @@ func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set content-type to default value if it is not set.
|
// Set content-type to default value if it is not set.
|
||||||
if _, ok := metadata["content-type"]; !ok {
|
if _, ok := metadata[strings.ToLower(xhttp.ContentType)]; !ok {
|
||||||
metadata["content-type"] = "application/octet-stream"
|
metadata[strings.ToLower(xhttp.ContentType)] = "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if contentEncoding, ok := metadata[strings.ToLower(xhttp.ContentEncoding)]; ok {
|
||||||
|
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||||
|
if contentEncoding != "" {
|
||||||
|
// Make sure to trim and save the content-encoding
|
||||||
|
// parameter for a streaming signature which is set
|
||||||
|
// to a custom value for example: "aws-chunked,gzip".
|
||||||
|
metadata[strings.ToLower(xhttp.ContentEncoding)] = contentEncoding
|
||||||
|
} else {
|
||||||
|
// Trimmed content encoding is empty when the header
|
||||||
|
// value is set to "aws-chunked" only.
|
||||||
|
|
||||||
|
// Make sure to delete the content-encoding parameter
|
||||||
|
// for a streaming signature which is set to value
|
||||||
|
// for example: "aws-chunked"
|
||||||
|
delete(metadata, strings.ToLower(xhttp.ContentEncoding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ const (
|
|||||||
ContentDisposition = "Content-Disposition"
|
ContentDisposition = "Content-Disposition"
|
||||||
Authorization = "Authorization"
|
Authorization = "Authorization"
|
||||||
Action = "Action"
|
Action = "Action"
|
||||||
|
Range = "Range"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Non standard S3 HTTP response constants
|
// Non standard S3 HTTP response constants
|
||||||
@ -114,6 +115,18 @@ const (
|
|||||||
// Server-Status
|
// Server-Status
|
||||||
MinIOServerStatus = "x-minio-server-status"
|
MinIOServerStatus = "x-minio-server-status"
|
||||||
|
|
||||||
// Delete special flag
|
// Delete special flag to force delete a bucket
|
||||||
MinIOForceDelete = "x-minio-force-delete"
|
MinIOForceDelete = "x-minio-force-delete"
|
||||||
|
|
||||||
|
// Header indicates if the mtime should be preserved by client
|
||||||
|
MinIOSourceMTime = "x-minio-source-mtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common http query params S3 API
|
||||||
|
const (
|
||||||
|
VersionID = "versionId"
|
||||||
|
|
||||||
|
PartNumber = "partNumber"
|
||||||
|
|
||||||
|
UploadID = "uploadId"
|
||||||
)
|
)
|
||||||
|
@ -175,6 +175,17 @@ type GenericError struct {
|
|||||||
Bucket string
|
Bucket string
|
||||||
Object string
|
Object string
|
||||||
VersionID string
|
VersionID string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidArgument incorrect input argument
|
||||||
|
type InvalidArgument GenericError
|
||||||
|
|
||||||
|
func (e InvalidArgument) Error() string {
|
||||||
|
if e.Err != nil {
|
||||||
|
return "Invalid arguments provided for " + e.Bucket + "/" + e.Object + ": (" + e.Err.Error() + ")"
|
||||||
|
}
|
||||||
|
return "Invalid arguments provided for " + e.Bucket + "/" + e.Object
|
||||||
}
|
}
|
||||||
|
|
||||||
// BucketNotFound bucket does not exist.
|
// BucketNotFound bucket does not exist.
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||||
"github.com/minio/minio-go/v6/pkg/tags"
|
"github.com/minio/minio-go/v6/pkg/tags"
|
||||||
@ -36,11 +37,12 @@ type GetObjectInfoFn func(ctx context.Context, bucket, object string, opts Objec
|
|||||||
// ObjectOptions represents object options for ObjectLayer object operations
|
// ObjectOptions represents object options for ObjectLayer object operations
|
||||||
type ObjectOptions struct {
|
type ObjectOptions struct {
|
||||||
ServerSideEncryption encrypt.ServerSide
|
ServerSideEncryption encrypt.ServerSide
|
||||||
Versioned bool
|
Versioned bool // indicates if the bucket is versioned
|
||||||
VersionID string
|
VersionID string // Specifies the versionID which needs to be overwritten or read
|
||||||
UserDefined map[string]string
|
MTime time.Time // Is only set in POST/PUT operations
|
||||||
PartNumber int
|
UserDefined map[string]string // only set in case of POST/PUT operations
|
||||||
CheckCopyPrecondFn CheckCopyPreconditionFn
|
PartNumber int // only useful in case of GetObject/HeadObject
|
||||||
|
CheckCopyPrecondFn CheckCopyPreconditionFn // only set during CopyObject preconditional valuation
|
||||||
}
|
}
|
||||||
|
|
||||||
// BucketOptions represents bucket options for ObjectLayer bucket operations
|
// BucketOptions represents bucket options for ObjectLayer bucket operations
|
||||||
|
242
cmd/object-api-options.go
Normal file
242
cmd/object-api-options.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2017-2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// set encryption options for pass through to backend in the case of gateway and UserDefined metadata
|
||||||
|
func getDefaultOpts(header http.Header, copySource bool, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||||
|
var clientKey [32]byte
|
||||||
|
var sse encrypt.ServerSide
|
||||||
|
|
||||||
|
opts = ObjectOptions{UserDefined: metadata}
|
||||||
|
if copySource {
|
||||||
|
if crypto.SSECopy.IsRequested(header) {
|
||||||
|
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.ServerSideEncryption = encrypt.SSECopy(sse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if crypto.SSEC.IsRequested(header) {
|
||||||
|
clientKey, err = crypto.SSEC.ParseHTTP(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.ServerSideEncryption = sse
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
||||||
|
opts.ServerSideEncryption = encrypt.NewSSE()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ObjectOptions for GET calls from encryption headers
|
||||||
|
func getOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||||
|
var (
|
||||||
|
encryption encrypt.ServerSide
|
||||||
|
opts ObjectOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
var partNumber int
|
||||||
|
var err error
|
||||||
|
if pn := r.URL.Query().Get(xhttp.PartNumber); pn != "" {
|
||||||
|
partNumber, err = strconv.Atoi(pn)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
if partNumber <= 0 {
|
||||||
|
return opts, errInvalidArgument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vid := strings.TrimSpace(r.URL.Query().Get(xhttp.VersionID))
|
||||||
|
if vid != "" && vid != nullVersionID {
|
||||||
|
_, err := uuid.Parse(vid)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return opts, VersionNotFound{
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: object,
|
||||||
|
VersionID: vid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
derivedKey := deriveClientKey(key, bucket, object)
|
||||||
|
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
||||||
|
logger.CriticalIf(ctx, err)
|
||||||
|
return ObjectOptions{
|
||||||
|
ServerSideEncryption: encryption,
|
||||||
|
VersionID: vid,
|
||||||
|
PartNumber: partNumber,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// default case of passing encryption headers to backend
|
||||||
|
opts, err = getDefaultOpts(r.Header, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
opts.PartNumber = partNumber
|
||||||
|
opts.VersionID = vid
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
||||||
|
versioned := globalBucketVersioningSys.Enabled(bucket)
|
||||||
|
opts, err = getOpts(ctx, r, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
opts.Versioned = versioned
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ObjectOptions for PUT calls from encryption headers and metadata
|
||||||
|
func putOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||||
|
versioned := globalBucketVersioningSys.Enabled(bucket)
|
||||||
|
vid := strings.TrimSpace(r.URL.Query().Get(xhttp.VersionID))
|
||||||
|
if vid != "" && vid != nullVersionID {
|
||||||
|
_, err := uuid.Parse(vid)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return opts, VersionNotFound{
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: object,
|
||||||
|
VersionID: vid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mtime := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime))
|
||||||
|
if mtime != "" {
|
||||||
|
opts.MTime, err = time.Parse(time.RFC3339, mtime)
|
||||||
|
if err != nil {
|
||||||
|
return opts, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: object,
|
||||||
|
Err: fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts.MTime = UTCNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case of multipart custom format, the metadata needs to be checked in addition to header to see if it
|
||||||
|
// is SSE-S3 encrypted, primarily because S3 protocol does not require SSE-S3 headers in PutObjectPart calls
|
||||||
|
if GlobalGatewaySSE.SSES3() && (crypto.S3.IsRequested(r.Header) || crypto.S3.IsEncrypted(metadata)) {
|
||||||
|
return ObjectOptions{
|
||||||
|
ServerSideEncryption: encrypt.NewSSE(),
|
||||||
|
UserDefined: metadata,
|
||||||
|
VersionID: vid,
|
||||||
|
Versioned: versioned,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
opts, err = getOpts(ctx, r, bucket, object)
|
||||||
|
opts.VersionID = vid
|
||||||
|
opts.Versioned = versioned
|
||||||
|
opts.UserDefined = metadata
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if crypto.S3KMS.IsRequested(r.Header) {
|
||||||
|
keyID, context, err := crypto.S3KMS.ParseHTTP(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectOptions{}, err
|
||||||
|
}
|
||||||
|
sseKms, err := encrypt.NewSSEKMS(keyID, context)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectOptions{}, err
|
||||||
|
}
|
||||||
|
return ObjectOptions{
|
||||||
|
ServerSideEncryption: sseKms,
|
||||||
|
UserDefined: metadata,
|
||||||
|
VersionID: vid,
|
||||||
|
Versioned: versioned,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
// default case of passing encryption headers and UserDefined metadata to backend
|
||||||
|
opts, err = getDefaultOpts(r.Header, false, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
opts.VersionID = vid
|
||||||
|
opts.Versioned = versioned
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ObjectOptions for Copy calls with encryption headers provided on the target side and source side metadata
|
||||||
|
func copyDstOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||||
|
return putOpts(ctx, r, bucket, object, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ObjectOptions for Copy calls with encryption headers provided on the source side
|
||||||
|
func copySrcOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||||
|
var (
|
||||||
|
ssec encrypt.ServerSide
|
||||||
|
opts ObjectOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if GlobalGatewaySSE.SSEC() && crypto.SSECopy.IsRequested(r.Header) {
|
||||||
|
key, err := crypto.SSECopy.ParseHTTP(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
derivedKey := deriveClientKey(key, bucket, object)
|
||||||
|
ssec, err = encrypt.NewSSEC(derivedKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(ssec)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// default case of passing encryption headers to backend
|
||||||
|
opts, err := getDefaultOpts(r.Header, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
@ -159,7 +159,7 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get request range.
|
// Get request range.
|
||||||
rangeHeader := r.Header.Get("Range")
|
rangeHeader := r.Header.Get(xhttp.Range)
|
||||||
if rangeHeader != "" {
|
if rangeHeader != "" {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrUnsupportedRangeHeader), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrUnsupportedRangeHeader), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
@ -370,7 +370,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
// Get request range.
|
// Get request range.
|
||||||
var rs *HTTPRangeSpec
|
var rs *HTTPRangeSpec
|
||||||
rangeHeader := r.Header.Get("Range")
|
rangeHeader := r.Header.Get(xhttp.Range)
|
||||||
if rangeHeader != "" {
|
if rangeHeader != "" {
|
||||||
if rs, err = parseRequestRangeSpec(rangeHeader); err != nil {
|
if rs, err = parseRequestRangeSpec(rangeHeader); err != nil {
|
||||||
// Handle only errInvalidRange. Ignore other
|
// Handle only errInvalidRange. Ignore other
|
||||||
@ -552,7 +552,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
// Get request range.
|
// Get request range.
|
||||||
var rs *HTTPRangeSpec
|
var rs *HTTPRangeSpec
|
||||||
rangeHeader := r.Header.Get("Range")
|
rangeHeader := r.Header.Get(xhttp.Range)
|
||||||
if rangeHeader != "" {
|
if rangeHeader != "" {
|
||||||
if rs, err = parseRequestRangeSpec(rangeHeader); err != nil {
|
if rs, err = parseRequestRangeSpec(rangeHeader); err != nil {
|
||||||
// Handle only errInvalidRange. Ignore other
|
// Handle only errInvalidRange. Ignore other
|
||||||
@ -810,7 +810,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
cpSrcPath := r.Header.Get(xhttp.AmzCopySource)
|
cpSrcPath := r.Header.Get(xhttp.AmzCopySource)
|
||||||
var vid string
|
var vid string
|
||||||
if u, err := url.Parse(cpSrcPath); err == nil {
|
if u, err := url.Parse(cpSrcPath); err == nil {
|
||||||
vid = strings.TrimSpace(u.Query().Get("versionId"))
|
vid = strings.TrimSpace(u.Query().Get(xhttp.VersionID))
|
||||||
// Note that url.Parse does the unescaping
|
// Note that url.Parse does the unescaping
|
||||||
cpSrcPath = u.Path
|
cpSrcPath = u.Path
|
||||||
}
|
}
|
||||||
@ -1355,26 +1355,6 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
metadata[xhttp.AmzObjectTagging] = objTags
|
metadata[xhttp.AmzObjectTagging] = objTags
|
||||||
}
|
}
|
||||||
|
|
||||||
if rAuthType == authTypeStreamingSigned {
|
|
||||||
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
|
||||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
|
||||||
if contentEncoding != "" {
|
|
||||||
// Make sure to trim and save the content-encoding
|
|
||||||
// parameter for a streaming signature which is set
|
|
||||||
// to a custom value for example: "aws-chunked,gzip".
|
|
||||||
metadata["content-encoding"] = contentEncoding
|
|
||||||
} else {
|
|
||||||
// Trimmed content encoding is empty when the header
|
|
||||||
// value is set to "aws-chunked" only.
|
|
||||||
|
|
||||||
// Make sure to delete the content-encoding parameter
|
|
||||||
// for a streaming signature which is set to value
|
|
||||||
// for example: "aws-chunked"
|
|
||||||
delete(metadata, "content-encoding")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
md5hex = hex.EncodeToString(md5Bytes)
|
md5hex = hex.EncodeToString(md5Bytes)
|
||||||
sha256hex = ""
|
sha256hex = ""
|
||||||
@ -1729,7 +1709,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
|||||||
cpSrcPath := r.Header.Get(xhttp.AmzCopySource)
|
cpSrcPath := r.Header.Get(xhttp.AmzCopySource)
|
||||||
var vid string
|
var vid string
|
||||||
if u, err := url.Parse(cpSrcPath); err == nil {
|
if u, err := url.Parse(cpSrcPath); err == nil {
|
||||||
vid = strings.TrimSpace(u.Query().Get("versionId"))
|
vid = strings.TrimSpace(u.Query().Get(xhttp.VersionID))
|
||||||
// Note that url.Parse does the unescaping
|
// Note that url.Parse does the unescaping
|
||||||
cpSrcPath = u.Path
|
cpSrcPath = u.Path
|
||||||
}
|
}
|
||||||
@ -1761,8 +1741,8 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID := r.URL.Query().Get("uploadId")
|
uploadID := r.URL.Query().Get(xhttp.UploadID)
|
||||||
partIDString := r.URL.Query().Get("partNumber")
|
partIDString := r.URL.Query().Get(xhttp.PartNumber)
|
||||||
|
|
||||||
partID, err := strconv.Atoi(partIDString)
|
partID, err := strconv.Atoi(partIDString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2071,8 +2051,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID := r.URL.Query().Get("uploadId")
|
uploadID := r.URL.Query().Get(xhttp.UploadID)
|
||||||
partIDString := r.URL.Query().Get("partNumber")
|
partIDString := r.URL.Query().Get(xhttp.PartNumber)
|
||||||
|
|
||||||
partID, err := strconv.Atoi(partIDString)
|
partID, err := strconv.Atoi(partIDString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user