1414
1515"""Create / interact with Google Cloud Storage blobs."""
1616
17+ import base64
1718import copy
19+ import hashlib
1820from io import BytesIO
1921import json
2022import mimetypes
@@ -109,6 +111,23 @@ def path_helper(bucket_path, blob_name):
109111 """
110112 return bucket_path + '/o/' + quote (blob_name , safe = '' )
111113
114+ @staticmethod
115+ def _get_customer_encryption_headers (key ):
116+ """Builds customer encyrption key headers
117+
118+ :type key: str
119+ :param key: 32 byte key to build request key and hash.
120+ """
121+ headers = {}
122+ key_hash = base64 .encodestring (hashlib .sha256 (key .encode ('utf-8' ))
123+ .digest ()).rstrip ()
124+ encoded_key = base64 .encodestring (bytes (key .encode ('utf-8' ))).rstrip ()
125+ headers ['X-Goog-Encryption-Algorithm' ] = 'AES256'
126+ headers ['X-Goog-Encryption-Key' ] = encoded_key .decode ('utf-8' )
127+ headers ['X-Goog-Encryption-Key-Sha256' ] = key_hash .decode ('utf-8' )
128+
129+ return headers
130+
112131 @property
113132 def acl (self ):
114133 """Create our ACL on demand."""
@@ -276,17 +295,34 @@ def delete(self, client=None):
276295 """
277296 return self .bucket .delete_blob (self .name , client = client )
278297
279- def download_to_file (self , file_obj , client = None ):
298+ def download_to_file (self , file_obj , key = None , client = None ):
280299 """Download the contents of this blob into a file-like object.
281300
282301 .. note::
283302
284303 If the server-set property, :attr:`media_link`, is not yet
285304 initialized, makes an additional API request to load it.
286305
306+ Downloading a file that has been `customer-supplied
307+ <https://cloud.google.com/storage/docs/encryption#customer-supplied>`_
308+ encryption::
309+
310+ >>> from gcloud import storage
311+ >>> from gcloud.storage import Blob
312+
313+ >>> sc = storage.Client(project='my-project')
314+ >>> bucket = sc.get_bucket('my-bucket')
315+ >>> key = 'aa426195405adee2c8081bb9e7e74b19'
316+ >>> blob = Blob('secure-data', bucket)
317+ >>> with open('/tmp/my-secure-file', 'w') as file_obj:
318+ >>> blob.download_to_file(file_obj, key=key)
319+
287320 :type file_obj: file
288321 :param file_obj: A file handle to which to write the blob's data.
289322
323+ :type key: str
324+ :param key: Optional 32 byte key for customer-supplied encryption.
325+
290326 :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
291327 :param client: Optional. The client to use. If not passed, falls back
292328 to the ``client`` stored on the blob's bucket.
@@ -305,7 +341,11 @@ def download_to_file(self, file_obj, client=None):
305341 if self .chunk_size is not None :
306342 download .chunksize = self .chunk_size
307343
308- request = Request (download_url , 'GET' )
344+ headers = {}
345+ if key :
346+ headers .update (self ._get_customer_encryption_headers (key ))
347+
348+ request = Request (download_url , 'GET' , headers )
309349
310350 # Use the private ``_connection`` rather than the public
311351 # ``.connection``, since the public connection may be a batch. A
@@ -315,27 +355,46 @@ def download_to_file(self, file_obj, client=None):
315355 # it has all three (http, API_BASE_URL and build_api_url).
316356 download .initialize_download (request , client ._connection .http )
317357
318- def download_to_filename (self , filename , client = None ):
358+ def download_to_filename (self , filename , key = None , client = None ):
319359 """Download the contents of this blob into a named file.
320360
321361 :type filename: string
322362 :param filename: A filename to be passed to ``open``.
323363
364+ :type key: str
365+ :param key: Optional 32 byte key for customer-supplied encryption.
366+
324367 :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
325368 :param client: Optional. The client to use. If not passed, falls back
326369 to the ``client`` stored on the blob's bucket.
327370
328371 :raises: :class:`gcloud.exceptions.NotFound`
329372 """
330373 with open (filename , 'wb' ) as file_obj :
331- self .download_to_file (file_obj , client = client )
374+ self .download_to_file (file_obj , key = key , client = client )
332375
333376 mtime = time .mktime (self .updated .timetuple ())
334377 os .utime (file_obj .name , (mtime , mtime ))
335378
336- def download_as_string (self , client = None ):
379+ def download_as_string (self , key = None , client = None ):
337380 """Download the contents of this blob as a string.
338381
382+ Downloading a blob that has been `customer-supplied
383+ <https://cloud.google.com/storage/docs/encryption#customer-supplied>`_
384+ encryption::
385+
386+ >>> from gcloud import storage
387+ >>> from gcloud.storage import Blob
388+
389+ >>> sc = storage.Client(project='my-project')
390+ >>> bucket = sc.get_bucket('my-bucket')
391+ >>> key = 'aa426195405adee2c8081bb9e7e74b19'
392+ >>> blob = Blob('secure-data', bucket)
393+ >>> data = blob.download_as_string(file_obj, key=key)
394+
395+ :type key: str
396+ :param key: Optional 32 byte key for customer-supplied encryption.
397+
339398 :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
340399 :param client: Optional. The client to use. If not passed, falls back
341400 to the ``client`` stored on the blob's bucket.
@@ -345,7 +404,7 @@ def download_as_string(self, client=None):
345404 :raises: :class:`gcloud.exceptions.NotFound`
346405 """
347406 string_buffer = BytesIO ()
348- self .download_to_file (string_buffer , client = client )
407+ self .download_to_file (string_buffer , key = key , client = client )
349408 return string_buffer .getvalue ()
350409
351410 @staticmethod
@@ -358,7 +417,8 @@ def _check_response_error(request, http_response):
358417 raise make_exception (faux_response , http_response .content ,
359418 error_info = request .url )
360419
361- def upload_from_file (self , file_obj , rewind = False , size = None ,
420+ # pylint: disable=too-many-arguments,too-many-locals
421+ def upload_from_file (self , file_obj , rewind = False , size = None , key = None ,
362422 content_type = None , num_retries = 6 , client = None ):
363423 """Upload the contents of this blob from a file-like object.
364424
@@ -391,6 +451,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
391451 :func:`os.fstat`. (If the file handle is not from the
392452 filesystem this won't be possible.)
393453
454+ :type key: str
455+ :param key: Optional 32 byte key for customer-supplied encryption.
456+
394457 :type content_type: string or ``NoneType``
395458 :param content_type: Optional type of content being uploaded.
396459
@@ -434,6 +497,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
434497 'User-Agent' : connection .USER_AGENT ,
435498 }
436499
500+ if key :
501+ headers .update (self ._get_customer_encryption_headers (key ))
502+
437503 upload = Upload (file_obj , content_type , total_bytes ,
438504 auto_transfer = False )
439505
@@ -474,7 +540,7 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
474540 response_content = response_content .decode ('utf-8' )
475541 self ._set_properties (json .loads (response_content ))
476542
477- def upload_from_filename (self , filename , content_type = None ,
543+ def upload_from_filename (self , filename , content_type = None , key = None ,
478544 client = None ):
479545 """Upload this blob's contents from the content of a named file.
480546
@@ -500,6 +566,9 @@ def upload_from_filename(self, filename, content_type=None,
500566 :type content_type: string or ``NoneType``
501567 :param content_type: Optional type of content being uploaded.
502568
569+ :type key: str
570+ :param key: Optional 32 byte key for customer-supplied encryption.
571+
503572 :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
504573 :param client: Optional. The client to use. If not passed, falls back
505574 to the ``client`` stored on the blob's bucket.
@@ -509,10 +578,10 @@ def upload_from_filename(self, filename, content_type=None,
509578 content_type , _ = mimetypes .guess_type (filename )
510579
511580 with open (filename , 'rb' ) as file_obj :
512- self .upload_from_file (file_obj , content_type = content_type ,
581+ self .upload_from_file (file_obj , content_type = content_type , key = key ,
513582 client = client )
514583
515- def upload_from_string (self , data , content_type = 'text/plain' ,
584+ def upload_from_string (self , data , content_type = 'text/plain' , key = None ,
516585 client = None ):
517586 """Upload contents of this blob from the provided string.
518587
@@ -527,6 +596,19 @@ def upload_from_string(self, data, content_type='text/plain',
527596 `lifecycle <https://cloud.google.com/storage/docs/lifecycle>`_
528597 API documents for details.
529598
599+ Uploading a string that with `customer-supplied
600+ <https://cloud.google.com/storage/docs/encryption#customer-supplied>`_
601+ encryption::
602+
603+ >>> from gcloud import storage
604+ >>> from gcloud.storage import Blob
605+
606+ >>> sc = storage.Client(project='my-project')
607+ >>> bucket = sc.get_bucket('my-bucket')
608+ >>> key = 'aa426195405adee2c8081bb9e7e74b19'
609+ >>> blob = Blob('secure-data', bucket)
610+ >>> b.upload_from_string('my secure string', key=key)
611+
530612 :type data: bytes or text
531613 :param data: The data to store in this blob. If the value is
532614 text, it will be encoded as UTF-8.
@@ -535,6 +617,9 @@ def upload_from_string(self, data, content_type='text/plain',
535617 :param content_type: Optional type of content being uploaded. Defaults
536618 to ``'text/plain'``.
537619
620+ :type key: str
621+ :param key: Optional 32 byte key for customer-supplied encryption.
622+
538623 :type client: :class:`gcloud.storage.client.Client` or ``NoneType``
539624 :param client: Optional. The client to use. If not passed, falls back
540625 to the ``client`` stored on the blob's bucket.
@@ -545,7 +630,7 @@ def upload_from_string(self, data, content_type='text/plain',
545630 string_buffer .write (data )
546631 self .upload_from_file (file_obj = string_buffer , rewind = True ,
547632 size = len (data ), content_type = content_type ,
548- client = client )
633+ key = key , client = client )
549634
550635 def make_public (self , client = None ):
551636 """Make this blob public giving all users read access.
0 commit comments