Repository: minio/minio-py

Issue:
allow `copy_object` with `content-type` in metadata
Trying to use `copy_object` to copy an object from one bucket to another.

To keep the target object well functional, I need to preserve `Content-Type` and `Content-Encoding` headers.

`Content-Encoding` is possible passing it's value via `metadata`.

However, I did not succeed with `Content-Type`.

The only solution was to edit sources of [minio/helpers.py](https://github.com/minio/minio-py/blob/master/minio/helpers.py#L667) function `is_supported_header` adding value "content-type".

```python
# returns true if a standard supported header
def is_supported_header(key):
    ## Supported headers for object.
    supported_headers = [
       "cache-control",
       "content-encoding",
       "content-type",  # added one
       "content-disposition",
       "content-language",
       "x-amz-website-redirect-location",
            ## Add more supported headers here.
        ]
    return key.lower() in supported_headers
```

This might be the solution, but I am not sure, it does not break anything else.

Discussion:
kannappanr: @vlcinsky Please look at the solution suggested in #738 suggested by @poornas 
kannappanr: @vlcinsky Can you please fix Travis build issues?
vlcinsky: Yes, I will fix it now.

Anyway, I will assume, the tests preventing usage of content-type in metadata are to be fixed. Hopefully this does not have other consequencies in other parts of the code. 

Patch:
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..71b739e
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE README*
+recursive-include docs *.md
+recursive-include examples *.py
+recursive-include tests *.py *.sh
diff --git a/minio/api.py b/minio/api.py
index 4fc873e..6b7b10a 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -31,15 +31,12 @@ from __future__ import absolute_import
 import platform
 from threading import Thread
 
-from time import mktime
 from datetime import datetime, timedelta
 
 import io
 import json
 import os
 import itertools
-import codecs
-
 
 
 try:
@@ -56,8 +53,8 @@ import dateutil.parser
 from . import __title__, __version__
 from .compat import (urlsplit, queryencode,
                      range, basestring)
-from .error import (KnownResponseError, ResponseError, NoSuchBucket, AccessDenied,
-                    InvalidArgumentError, InvalidSizeError, InvalidXMLError, NoSuchBucketPolicy)
+from .error import (ResponseError, NoSuchBucket, AccessDenied,
+                    InvalidArgumentError, InvalidSizeError, InvalidXMLError)
 from .definitions import Object, UploadPart
 from .parsers import (parse_list_buckets,
                       parse_list_objects,
@@ -72,13 +69,12 @@ from .parsers import (parse_list_buckets,
                       parse_multi_object_delete_response)
 from .helpers import (get_target_url, is_non_empty_string,
                       is_valid_endpoint,
-                      get_sha256_hexdigest, get_md5_base64digest, Hasher,
+                      get_sha256_hexdigest, get_md5_base64digest,
                       optimal_part_info,
-                      is_valid_bucket_name, PartMetadata, read_full,
+                      is_valid_bucket_name, read_full,
                       get_s3_region_from_endpoint, is_valid_sse_object, is_valid_sse_c_object,
                       is_valid_source_sse_object,
                       is_valid_bucket_notification_config, is_valid_policy_type,
-                      get_s3_region_from_endpoint,
                       mkdir_p, dump_http, amzprefix_user_metadata,
                       is_supported_header,is_amz_header)
 from .helpers import (MAX_MULTIPART_OBJECT_SIZE,
@@ -130,6 +126,7 @@ class Minio(object):
     :param endpoint: Hostname of the cloud storage server.
     :param access_key: Access key to sign self._http.request with.
     :param secret_key: Secret key to sign self._http.request with.
+    :param session_token: Session token to sign self._http.request with.
     :param secure: Set this value if wish to make secure requests.
          Default is True.
     :param region: Set this value to override automatic bucket
@@ -139,7 +136,9 @@ class Minio(object):
     :return: :class:`Minio <Minio>` object
     """
     def __init__(self, endpoint, access_key=None,
-                 secret_key=None, secure=True,
+                 secret_key=None,
+                 session_token=None,
+                 secure=True,
                  region=None,
                  http_client=None):
 
@@ -169,6 +168,7 @@ class Minio(object):
         self._is_ssl = secure
         self._access_key = access_key
         self._secret_key = secret_key
+        self._session_token = session_token
         self._user_agent = _DEFAULT_USER_AGENT
         self._trace_output_stream = None
 
@@ -287,7 +287,9 @@ class Minio(object):
         # Get signature headers if any.
         headers = sign_v4(method, url, region,
                           headers, self._access_key,
-                          self._secret_key, content_sha256_hex)
+                          self._secret_key,
+                          self._session_token,
+                          content_sha256_hex)
 
         response = self._http.urlopen(method, url,
                                       body=content,
@@ -324,7 +326,9 @@ class Minio(object):
         # Get signature headers if any.
         headers = sign_v4(method, url, region,
                           headers, self._access_key,
-                          self._secret_key, None)
+                          self._secret_key,
+                          self._session_token,
+                          None)
 
         response = self._http.urlopen(method, url,
                                       body=None,
@@ -354,9 +358,9 @@ class Minio(object):
         try:
             self._url_open('HEAD', bucket_name=bucket_name)
         # If the bucket has not been created yet, Minio will return a "NoSuchBucket" error.
-        except NoSuchBucket as e:
+        except NoSuchBucket:
             return False
-        except ResponseError as e:
+        except ResponseError:
             raise
         return True
 
@@ -1293,6 +1297,7 @@ class Minio(object):
         return presign_v4(method, url,
                           self._access_key,
                           self._secret_key,
+                          session_token=self._session_token,
                           region=region,
                           expires=int(expires.total_seconds()),
                           response_headers=response_headers,
@@ -1384,21 +1389,29 @@ class Minio(object):
         credential_string = generate_credential_string(self._access_key,
                                                        date, region)
 
-        post_policy_base64 = post_policy.base64(extras=[
+        policy = [
             ('eq', '$x-amz-date', iso8601_date),
             ('eq', '$x-amz-algorithm', _SIGN_V4_ALGORITHM),
             ('eq', '$x-amz-credential', credential_string),
-        ])
+        ]
+        if self._session_token:
+            policy.add(('eq', '$x-amz-security-token', self._session_token))
+
+        post_policy_base64 = post_policy.base64(extras=policy)
         signature = post_presign_signature(date, region,
                                            self._secret_key,
                                            post_policy_base64)
-        post_policy.form_data.update({
+        form_data = {
             'policy': post_policy_base64,
             'x-amz-algorithm': _SIGN_V4_ALGORITHM,
             'x-amz-credential': credential_string,
             'x-amz-date': iso8601_date,
             'x-amz-signature': signature,
-        })
+        }
+        if self._session_token:
+            form_data['x-amz-security-token'] = self._session_token
+
+        post_policy.form_data.update(form_data)
         url_str = get_target_url(self._endpoint_url,
                                  bucket_name=post_policy.form_data['bucket'],
                                  bucket_region=region)
@@ -1779,7 +1792,9 @@ class Minio(object):
         # Get signature headers if any.
         headers = sign_v4(method, url, region,
                           headers, self._access_key,
-                          self._secret_key, None)
+                          self._secret_key,
+                          self._session_token,
+                          None)
 
         response = self._http.urlopen(method, url,
                                       body=None,
@@ -1828,7 +1843,8 @@ class Minio(object):
         # Get signature headers if any.
         headers = sign_v4(method, url, region,
                           fold_case_headers, self._access_key,
-                          self._secret_key, content_sha256)
+                          self._secret_key, self._session_token,
+                          content_sha256)
 
         response = self._http.urlopen(method, url,
                                       body=body,
diff --git a/minio/helpers.py b/minio/helpers.py
index 6f3a6af..fe49543 100644
--- a/minio/helpers.py
+++ b/minio/helpers.py
@@ -665,6 +665,7 @@ def is_supported_header(key):
     supported_headers = [
 	   "cache-control",
 	   "content-encoding",
+	   "content-type",
 	   "content-disposition",
 	   "content-language",
 	   "x-amz-website-redirect-location",
diff --git a/minio/signer.py b/minio/signer.py
index ad66968..ef6bf7f 100644
--- a/minio/signer.py
+++ b/minio/signer.py
@@ -58,8 +58,8 @@ def post_presign_signature(date, region, secret_key, policy_str):
     return signature
 
 
-def presign_v4(method, url, access_key, secret_key, region=None,
-               headers=None, expires=None, response_headers=None,
+def presign_v4(method, url, access_key, secret_key, session_token=None,
+               region=None, headers=None, expires=None, response_headers=None,
                request_date=None):
     """
     Calculates signature version '4' for regular presigned URLs.
@@ -67,7 +67,9 @@ def presign_v4(method, url, access_key, secret_key, region=None,
     :param method: Method to be presigned examples 'PUT', 'GET'.
     :param url: URL to be presigned.
     :param access_key: Access key id for your AWS s3 account.
-    :param secret_key: Secrect access key for your AWS s3 account.
+    :param secret_key: Secret access key for your AWS s3 account.
+    :param session_token: Session token key set only for temporary
+       access credentials.
     :param region: region of the bucket, it is optional.
     :param headers: any additional HTTP request headers to
        be presigned, it is optional.
@@ -107,6 +109,9 @@ def presign_v4(method, url, access_key, secret_key, region=None,
                                                            region)
     query['X-Amz-Date'] = iso8601Date
     query['X-Amz-Expires'] = str(expires)
+    if session_token:
+        query['X-Amz-Security-Token'] = session_token
+
     signed_headers = get_signed_headers(headers_to_sign)
     query['X-Amz-SignedHeaders'] = ';'.join(signed_headers)
 
@@ -164,8 +169,11 @@ def get_signed_headers(headers):
     return sorted(signed_headers)
 
 
-def sign_v4(method, url, region, headers=None, access_key=None,
-            secret_key=None, content_sha256=None):
+def sign_v4(method, url, region, headers=None,
+            access_key=None,
+            secret_key=None,
+            session_token=None,
+            content_sha256=None):
     """
     Signature version 4.
 
@@ -177,6 +185,8 @@ def sign_v4(method, url, region, headers=None, access_key=None,
        specified no signature is needed.
     :param secret_key: Optional secret key, if not
        specified no signature is needed.
+    :param session_token: Optional session token, set
+       only for temporary credentials.
     :param content_sha256: Optional body sha256.
     """
 
@@ -204,6 +214,8 @@ def sign_v4(method, url, region, headers=None, access_key=None,
     date = datetime.utcnow()
     headers['X-Amz-Date'] = date.strftime("%Y%m%dT%H%M%SZ")
     headers['X-Amz-Content-Sha256'] = content_sha256
+    if session_token:
+        headers['X-Amz-Security-Token'] = session_token
 
     headers_to_sign = headers
 
diff --git a/minio/sse.py b/minio/sse.py
index c6700cf..d8be994 100644
--- a/minio/sse.py
+++ b/minio/sse.py
@@ -26,7 +26,7 @@ This module contains core API parsers.
 import base64
 import hashlib
 import json
-from .error import (InvalidArgumentError, InvalidSizeError, InvalidXMLError, NoSuchBucketPolicy)
+from .error import (InvalidArgumentError, InvalidSizeError)
 
 class SSE_C(object):
 
@@ -34,10 +34,10 @@ class SSE_C(object):
         self.key = key
         if len(self.key) != 32:
             raise InvalidSizeError("SSE-C keys need to be 256 bit base64 encoded")
-    
+
     def type(self):
         return "SSE-C"
-    
+
     def marshal(self):
 
         b64key = base64.b64encode(self.key)
@@ -95,9 +95,9 @@ class SSE_KMS(object):
                 b64key = base64.b64encode(ctx_str)
                 header = {"X-Amz-Server-Side-Encryption-Context": b64key.decode()}
                 keys.update(header)
-        
+
         return keys
-        
+
 
 class SSE_S3(object):
     def type(self):
@@ -108,4 +108,3 @@ class SSE_S3(object):
             "X-Amz-Server-Side-Encryption":"AES256"
         }
         return keys
-    


Test patch:
diff --git a/tests/unit/header_value_test.py b/tests/unit/header_value_test.py
index 0c7cbd3..28f2768 100644
--- a/tests/unit/header_value_test.py
+++ b/tests/unit/header_value_test.py
@@ -20,25 +20,100 @@ from unittest import TestCase
 from minio.helpers import is_storageclass_header,is_amz_header,is_supported_header,amzprefix_user_metadata
 
 class HeaderTests(TestCase):
+    header_variants = {
+        "content-type": [
+            "content-type",
+            "Content-Type",
+            "CONTENT-TYPE",
+            "cONTENT-tYPE",
+            "cOntent-TypE",
+            "CoNTENT-tYPe",
+        ],
+        "x-amz-meta-me": [
+            "x-amz-meta-me",
+            "X-Amz-Meta-Me",
+            "X-AMZ-META-ME",
+            "x-aMZ-mETA-mE",
+        ],
+        "cache-control": [
+            "cache-control" ,
+            "Cache-Control" ,
+            "CACHE-CONTROL" ,
+            "cACHE-cONTROL" ,
+            "CacHe-conTrol" ,
+        ],
+        "content-disposition": [
+            "content-disposition",
+            "Content-Disposition",
+            "CONTENT-DISPOSITION",
+            "cONTENT-dISPOSITION",
+            "conTent-disPositioN",
+        ],
+        "content-language": [
+            "content-language",
+            "Content-Language",
+            "CONTENT-LANGUAGE",
+            "conTent-Language",
+        ],
+        "x-amz-website-redirect-location": [
+            "x-amz-website-redirect-location",
+            "X-Amz-Website-Redirect-Location",
+            "X-AMZ-WEBSITE-REDIRECT-LOCATION",
+            "x-aMZ-wEBSITE-rEDIRECT-lOCATION",
+        ],
+        "x-amz-meta-status-code": [
+            "x-amz-meta-status-code",
+            "X-Amz-Meta-Status-Code",
+            "X-AMZ-META-STATUS-CODE",
+            "x-aMZ-mETA-sTATUS-cODE",
+        ],
+        "x-amz-server-side-encryption": [
+            "x-amz-server-side-encryption",
+            "X-Amz-Server-Side-Encryption",
+            "X-AMZ-SERVER-SIDE-ENCRYPTION",
+            "x-aMZ-sERVER-sIDE-eNCRYPTION",
+        ],
+        "x-amz-storage-class": [
+            "x-amz-storage-class",
+            "X-Amz-Storage-Class",
+            "X-AMZ-STORAGE-CLASS",
+            "x-aMZ-sTORAGE-cLASS",
+        ],
+    }
+
+    def check_ok_header(self, check_fun, header):
+        for header_variant in self.header_variants.get(header, [header]):
+            eq_(check_fun(header_variant), True)
+
+    def check_bad_header(self, check_fun, header):
+        for header_variant in self.header_variants.get(header, [header]):
+            eq_(check_fun(header_variant), False)
+
     def test_is_supported_header(self):
-        eq_(is_supported_header("content-type"),False)
-        eq_(is_supported_header("Content-Type"),False)
-        eq_(is_supported_header("cOntent-TypE"),False)
-        eq_(is_supported_header("x-amz-meta-me"),False)
-        eq_(is_supported_header("Cache-Control"),True)
-        eq_(is_supported_header("content-encoding"),True)
-        eq_(is_supported_header("content-disposition"),True)
-        eq_(is_supported_header("content-language"),True)
-        eq_(is_supported_header("x-amz-website-redirect-location"),True)
+        self.check_ok_header(is_supported_header, "content-type")
+        self.check_ok_header(is_supported_header, "cache-control")
+        self.check_ok_header(is_supported_header, "content-disposition")
+        self.check_ok_header(is_supported_header, "content-encoding")
+        self.check_ok_header(is_supported_header, "content-language")
+        self.check_ok_header(is_supported_header, "x-amz-website-redirect-location")
+
+    def test_is_not_supported_header(self):
+        self.check_bad_header(is_supported_header, "x-amz-meta-me")
+
     def test_is_amz_header(self):
-        eq_(is_amz_header("x-amz-meta-status-code"),True)
-        eq_(is_amz_header("X-Amz-Meta-status-code"),True)
-        eq_(is_amz_header("X_AMZ_META-VALUE"),False)
-        eq_(is_amz_header("content-type"),False)
-        eq_(is_amz_header("x-amz-server-side-encryption"),True)
+        self.check_ok_header(is_amz_header, "x-amz-meta-status-code")
+        self.check_ok_header(is_amz_header, "x-amz-server-side-encryption")
+
+    def test_is_not_amz_header(self):
+        self.check_bad_header(is_amz_header, "X_AMZ_META-VALUE")
+        self.check_bad_header(is_amz_header, "content-type")
+
     def test_is_storageclass_header(self):
-        eq_(is_storageclass_header("x-amz-storage-classs"),False)
-        eq_(is_storageclass_header("x-amz-storage-class"),True)
+        self.check_ok_header(is_storageclass_header, "x-amz-storage-class")
+
+    def test_is_not_storageclass_header(self):
+        self.check_bad_header(is_storageclass_header, "x-amz-storage-classs")
+
     def test_amzprefix_user_metadata(self):
         metadata = {
                   'x-amz-meta-testing': 'values',
@@ -52,8 +127,9 @@ class HeaderTests(TestCase):
                   'content-language':'fr'
                   }
         m = amzprefix_user_metadata(metadata)
-        self.assertTrue('X-Amz-Meta-hhh',m)
-        self.assertTrue('Content-Type',m)
-        self.assertTrue('x-amz-storage-class',m)
-        self.assertTrue('content-language',m)
-        self.assertTrue('X-Amz-Meta-amz-meta-setting',m)
+        self.assertTrue('Content-Type' in m)
+        self.assertTrue('content-language' in m)
+
+        self.assertTrue('X-Amz-Meta-hhh' in m)
+        self.assertTrue('x-amz-storage-class' in m)
+        self.assertTrue('X-Amz-Meta-amz-meta-setting' in m)