mirror of
https://github.com/minio/minio.git
synced 2025-02-03 18:06:00 -05:00
Fix azure metadata handling for all incoming PUT requests (#5038)
s3cmd cli fails when trying to upload a file to azure gateway. Previous fixes in azure to handle client side encryption alone did not completely address the problem. We need to possibilly convert all the x-amz-meta-<name> , i.e specifically <name> should be converted into a C# identifier as mentioned in the docs for `put-blob`. https://docs.microsoft.com/en-us/rest/api/storageservices/put-blob ``` s3cmd put README.md s3://myanis/ upload: 'README.md' -> 's3://myanis/README.md' [1 of 1] 4598 of 4598 100% in 0s 47.24 kB/s done upload: 'README.md' -> 's3://myanis/README.md' [1 of 1] 4598 of 4598 100% in 0s 50.47 kB/s done ERROR: S3 error: 400 (InvalidArgument): Your metadata headers are not supported. ``` There is a separate issue with s3cmd after this fix is applied where the ETag is wronly validated https://github.com/s3tools/s3cmd/issues/880 But that is an upstream s3cmd problem which wrongly interprets ETag to be md5sum of the content that was uploaded.
This commit is contained in:
parent
d5895d3243
commit
45463b1d6b
@ -55,31 +55,37 @@ const metadataObjectNameTemplate = globalMinioSysTmp + "multipart/v1/%s.%x/azure
|
|||||||
//
|
//
|
||||||
// Header names are canonicalized as in http.Header.
|
// Header names are canonicalized as in http.Header.
|
||||||
func s3MetaToAzureProperties(s3Metadata map[string]string) (storage.BlobMetadata,
|
func s3MetaToAzureProperties(s3Metadata map[string]string) (storage.BlobMetadata,
|
||||||
storage.BlobProperties) {
|
storage.BlobProperties, error) {
|
||||||
|
for k := range s3Metadata {
|
||||||
// Azure does not permit user-defined metadata key names to
|
if strings.Contains(k, "--") {
|
||||||
// contain hyphens. So we map hyphens to underscores for
|
return storage.BlobMetadata{}, storage.BlobProperties{}, traceError(UnsupportedMetadata{})
|
||||||
// encryption headers. More such headers may be added in the
|
}
|
||||||
// future.
|
|
||||||
gatewayHeaders := map[string]string{
|
|
||||||
"X-Amz-Meta-X-Amz-Key": "X-Amz-Meta-x_minio_key",
|
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "X-Amz-Meta-x_minio_matdesc",
|
|
||||||
"X-Amz-Meta-X-Amz-Iv": "X-Amz-Meta-x_minio_iv",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encoding technique for each key is used here is as follows
|
||||||
|
// Each '-' is converted to '_'
|
||||||
|
// Each '_' is converted to '__'
|
||||||
|
// With this basic assumption here are some of the expected
|
||||||
|
// translations for these keys.
|
||||||
|
// i: 'x-S3cmd_attrs' -> o: 'x_s3cmd__attrs' (mixed)
|
||||||
|
// i: 'x__test__value' -> o: 'x____test____value' (double '_')
|
||||||
|
encodeKey := func(key string) string {
|
||||||
|
tokens := strings.Split(key, "_")
|
||||||
|
for i := range tokens {
|
||||||
|
tokens[i] = strings.Replace(tokens[i], "-", "_", -1)
|
||||||
|
}
|
||||||
|
return strings.Join(tokens, "__")
|
||||||
|
}
|
||||||
var blobMeta storage.BlobMetadata = make(map[string]string)
|
var blobMeta storage.BlobMetadata = make(map[string]string)
|
||||||
var props storage.BlobProperties
|
var props storage.BlobProperties
|
||||||
for k, v := range s3Metadata {
|
for k, v := range s3Metadata {
|
||||||
k = http.CanonicalHeaderKey(k)
|
k = http.CanonicalHeaderKey(k)
|
||||||
if nk, ok := gatewayHeaders[k]; ok {
|
|
||||||
k = nk
|
|
||||||
}
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(k, "X-Amz-Meta-"):
|
case strings.HasPrefix(k, "X-Amz-Meta-"):
|
||||||
// Strip header prefix, to let Azure SDK
|
// Strip header prefix, to let Azure SDK
|
||||||
// handle it for storage.
|
// handle it for storage.
|
||||||
k = strings.Replace(k, "X-Amz-Meta-", "", 1)
|
k = strings.Replace(k, "X-Amz-Meta-", "", 1)
|
||||||
blobMeta[k] = v
|
blobMeta[encodeKey(k)] = v
|
||||||
|
|
||||||
// All cases below, extract common metadata that is
|
// All cases below, extract common metadata that is
|
||||||
// accepted by S3 into BlobProperties for setting on
|
// accepted by S3 into BlobProperties for setting on
|
||||||
@ -100,7 +106,7 @@ func s3MetaToAzureProperties(s3Metadata map[string]string) (storage.BlobMetadata
|
|||||||
props.ContentType = v
|
props.ContentType = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return blobMeta, props
|
return blobMeta, props, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// azurePropertiesToS3Meta converts Azure metadata/properties to S3
|
// azurePropertiesToS3Meta converts Azure metadata/properties to S3
|
||||||
@ -108,22 +114,26 @@ func s3MetaToAzureProperties(s3Metadata map[string]string) (storage.BlobMetadata
|
|||||||
// `.GetMetadata()` lower-cases all header keys, so this is taken into
|
// `.GetMetadata()` lower-cases all header keys, so this is taken into
|
||||||
// account by this function.
|
// account by this function.
|
||||||
func azurePropertiesToS3Meta(meta storage.BlobMetadata, props storage.BlobProperties) map[string]string {
|
func azurePropertiesToS3Meta(meta storage.BlobMetadata, props storage.BlobProperties) map[string]string {
|
||||||
// Remap underscores to hyphens to restore encryption
|
// Decoding technique for each key is used here is as follows
|
||||||
// headers. See s3MetaToAzureProperties for details.
|
// Each '_' is converted to '-'
|
||||||
gatewayHeaders := map[string]string{
|
// Each '__' is converted to '_'
|
||||||
"X-Amz-Meta-x_minio_key": "X-Amz-Meta-X-Amz-Key",
|
// With this basic assumption here are some of the expected
|
||||||
"X-Amz-Meta-x_minio_matdesc": "X-Amz-Meta-X-Amz-Matdesc",
|
// translations for these keys.
|
||||||
"X-Amz-Meta-x_minio_iv": "X-Amz-Meta-X-Amz-Iv",
|
// i: 'x_s3cmd__attrs' -> o: 'x-s3cmd_attrs' (mixed)
|
||||||
|
// i: 'x____test____value' -> o: 'x__test__value' (double '_')
|
||||||
|
decodeKey := func(key string) string {
|
||||||
|
tokens := strings.Split(key, "__")
|
||||||
|
for i := range tokens {
|
||||||
|
tokens[i] = strings.Replace(tokens[i], "_", "-", -1)
|
||||||
|
}
|
||||||
|
return strings.Join(tokens, "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
s3Metadata := make(map[string]string)
|
s3Metadata := make(map[string]string)
|
||||||
for k, v := range meta {
|
for k, v := range meta {
|
||||||
// k's `x-ms-meta-` prefix is already stripped by
|
// k's `x-ms-meta-` prefix is already stripped by
|
||||||
// Azure SDK, so we add the AMZ prefix.
|
// Azure SDK, so we add the AMZ prefix.
|
||||||
k = "X-Amz-Meta-" + k
|
k = "X-Amz-Meta-" + decodeKey(k)
|
||||||
if nk, ok := gatewayHeaders[k]; ok {
|
|
||||||
k = nk
|
|
||||||
}
|
|
||||||
k = http.CanonicalHeaderKey(k)
|
k = http.CanonicalHeaderKey(k)
|
||||||
s3Metadata[k] = v
|
s3Metadata[k] = v
|
||||||
}
|
}
|
||||||
@ -514,7 +524,10 @@ func (a *azureObjects) GetObjectInfo(bucket, object string) (objInfo ObjectInfo,
|
|||||||
func (a *azureObjects) PutObject(bucket, object string, data *HashReader, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
func (a *azureObjects) PutObject(bucket, object string, data *HashReader, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
||||||
delete(metadata, "etag")
|
delete(metadata, "etag")
|
||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
blob.Metadata, blob.Properties = s3MetaToAzureProperties(metadata)
|
blob.Metadata, blob.Properties, err = s3MetaToAzureProperties(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
err = blob.CreateBlockBlobFromReader(data, nil)
|
err = blob.CreateBlockBlobFromReader(data, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
||||||
@ -533,7 +546,10 @@ func (a *azureObjects) PutObject(bucket, object string, data *HashReader, metada
|
|||||||
func (a *azureObjects) CopyObject(srcBucket, srcObject, destBucket, destObject string, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
func (a *azureObjects) CopyObject(srcBucket, srcObject, destBucket, destObject string, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
||||||
srcBlobURL := a.client.GetContainerReference(srcBucket).GetBlobReference(srcObject).GetURL()
|
srcBlobURL := a.client.GetContainerReference(srcBucket).GetBlobReference(srcObject).GetURL()
|
||||||
destBlob := a.client.GetContainerReference(destBucket).GetBlobReference(destObject)
|
destBlob := a.client.GetContainerReference(destBucket).GetBlobReference(destObject)
|
||||||
azureMeta, props := s3MetaToAzureProperties(metadata)
|
azureMeta, props, err := s3MetaToAzureProperties(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||||
|
}
|
||||||
destBlob.Metadata = azureMeta
|
destBlob.Metadata = azureMeta
|
||||||
err = destBlob.Copy(srcBlobURL, nil)
|
err = destBlob.Copy(srcBlobURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -587,10 +603,6 @@ func (a *azureObjects) checkUploadIDExists(bucketName, objectName, uploadID stri
|
|||||||
|
|
||||||
// NewMultipartUpload - Use Azure equivalent CreateBlockBlob.
|
// NewMultipartUpload - Use Azure equivalent CreateBlockBlob.
|
||||||
func (a *azureObjects) NewMultipartUpload(bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
func (a *azureObjects) NewMultipartUpload(bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
||||||
if metadata == nil {
|
|
||||||
metadata = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadID = mustGetAzureUploadID()
|
uploadID = mustGetAzureUploadID()
|
||||||
if err = a.checkUploadIDExists(bucket, object, uploadID); err == nil {
|
if err = a.checkUploadIDExists(bucket, object, uploadID); err == nil {
|
||||||
return "", traceError(errors.New("Upload ID name collision"))
|
return "", traceError(errors.New("Upload ID name collision"))
|
||||||
@ -778,7 +790,10 @@ func (a *azureObjects) CompleteMultipartUpload(bucket, object, uploadID string,
|
|||||||
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
if len(metadata.Metadata) > 0 {
|
if len(metadata.Metadata) > 0 {
|
||||||
objBlob.Metadata, objBlob.Properties = s3MetaToAzureProperties(metadata.Metadata)
|
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
|
}
|
||||||
err = objBlob.SetProperties(nil)
|
err = objBlob.SetProperties(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
||||||
|
@ -48,6 +48,9 @@ func TestS3MetaToAzureProperties(t *testing.T) {
|
|||||||
"accept-encoding": "gzip",
|
"accept-encoding": "gzip",
|
||||||
"content-encoding": "gzip",
|
"content-encoding": "gzip",
|
||||||
"X-Amz-Meta-Hdr": "value",
|
"X-Amz-Meta-Hdr": "value",
|
||||||
|
"X-Amz-Meta-X_test_key": "value",
|
||||||
|
"X-Amz-Meta-X__test__key": "value",
|
||||||
|
"X-Amz-Meta-X-Test__key": "value",
|
||||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
@ -55,28 +58,51 @@ func TestS3MetaToAzureProperties(t *testing.T) {
|
|||||||
// Only X-Amz-Meta- prefixed entries will be returned in
|
// Only X-Amz-Meta- prefixed entries will be returned in
|
||||||
// Metadata (without the prefix!)
|
// Metadata (without the prefix!)
|
||||||
expectedHeaders := map[string]string{
|
expectedHeaders := map[string]string{
|
||||||
"Hdr": "value",
|
"Hdr": "value",
|
||||||
"x_minio_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
"X__test__key": "value",
|
||||||
"x_minio_matdesc": "{}",
|
"X____test____key": "value",
|
||||||
"x_minio_iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
"X_Test____key": "value",
|
||||||
|
"X_Amz_Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"X_Amz_Matdesc": "{}",
|
||||||
|
"X_Amz_Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
}
|
||||||
|
meta, _, err := s3MetaToAzureProperties(headers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test failed, with %s", err)
|
||||||
}
|
}
|
||||||
meta, _ := s3MetaToAzureProperties(headers)
|
|
||||||
if !reflect.DeepEqual(map[string]string(meta), expectedHeaders) {
|
if !reflect.DeepEqual(map[string]string(meta), expectedHeaders) {
|
||||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, meta)
|
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, meta)
|
||||||
}
|
}
|
||||||
|
headers = map[string]string{
|
||||||
|
"invalid--meta": "value",
|
||||||
|
}
|
||||||
|
_, _, err = s3MetaToAzureProperties(headers)
|
||||||
|
if err = errorCause(err); err != nil {
|
||||||
|
if _, ok := err.(UnsupportedMetadata); !ok {
|
||||||
|
t.Fatalf("Test failed with unexpected error %s, expected UnsupportedMetadata", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAzurePropertiesToS3Meta(t *testing.T) {
|
func TestAzurePropertiesToS3Meta(t *testing.T) {
|
||||||
// Just one testcase. Adding more test cases does not add value to the testcase
|
// Just one testcase. Adding more test cases does not add value to the testcase
|
||||||
// as azureToS3Metadata() just adds a prefix.
|
// as azureToS3Metadata() just adds a prefix.
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
"First-Name": "myname",
|
"first_name": "myname",
|
||||||
"x_minio_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
"x_test_key": "value",
|
||||||
"x_minio_matdesc": "{}",
|
"x_test__key": "value",
|
||||||
"x_minio_iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
"x__test__key": "value",
|
||||||
|
"x____test____key": "value",
|
||||||
|
"x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"x_amz_matdesc": "{}",
|
||||||
|
"x_amz_iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
}
|
}
|
||||||
expectedMeta := map[string]string{
|
expectedMeta := map[string]string{
|
||||||
"X-Amz-Meta-First-Name": "myname",
|
"X-Amz-Meta-First-Name": "myname",
|
||||||
|
"X-Amz-Meta-X-Test-Key": "value",
|
||||||
|
"X-Amz-Meta-X-Test_key": "value",
|
||||||
|
"X-Amz-Meta-X_test_key": "value",
|
||||||
|
"X-Amz-Meta-X__test__key": "value",
|
||||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user