14
14
15
15
"""Create / interact with Google Cloud Storage blobs."""
16
16
17
+ import base64
17
18
import copy
19
+ import hashlib
18
20
from io import BytesIO
19
21
import json
20
22
import mimetypes
26
28
from six .moves .urllib .parse import quote
27
29
28
30
from gcloud ._helpers import _rfc3339_to_datetime
31
+ from gcloud ._helpers import _to_bytes
32
+ from gcloud ._helpers import _bytes_to_unicode
29
33
from gcloud .credentials import generate_signed_url
30
34
from gcloud .exceptions import NotFound
31
35
from gcloud .exceptions import make_exception
@@ -276,17 +280,41 @@ def delete(self, client=None):
276
280
"""
277
281
return self .bucket .delete_blob (self .name , client = client )
278
282
279
- def download_to_file (self , file_obj , client = None ):
283
+ def download_to_file (self , file_obj , encryption_key = None , client = None ):
280
284
"""Download the contents of this blob into a file-like object.
281
285
282
286
.. note::
283
287
284
288
If the server-set property, :attr:`media_link`, is not yet
285
289
initialized, makes an additional API request to load it.
286
290
291
+ Downloading a file that has been encrypted with a `customer-supplied`_
292
+ encryption key::
293
+
294
+ >>> from gcloud import storage
295
+ >>> from gcloud.storage import Blob
296
+
297
+ >>> client = storage.Client(project='my-project')
298
+ >>> bucket = client.get_bucket('my-bucket')
299
+ >>> encryption_key = 'aa426195405adee2c8081bb9e7e74b19'
300
+ >>> blob = Blob('secure-data', bucket)
301
+ >>> with open('/tmp/my-secure-file', 'wb') as file_obj:
302
+ >>> blob.download_to_file(file_obj,
303
+ ... encryption_key=encryption_key)
304
+
305
+ The ``encryption_key`` should be a str or bytes with a length of at
306
+ least 32.
307
+
308
+ .. _customer-supplied: https://cloud.google.com/storage/docs/\
309
+ encryption#customer-supplied
310
+
287
311
:type file_obj: file
288
312
:param file_obj: A file handle to which to write the blob's data.
289
313
314
+ :type encryption_key: str or bytes
315
+ :param encryption_key: Optional 32 byte encryption key for
316
+ customer-supplied encryption.
317
+
290
318
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
291
319
:param client: Optional. The client to use. If not passed, falls back
292
320
to the ``client`` stored on the blob's bucket.
@@ -305,7 +333,11 @@ def download_to_file(self, file_obj, client=None):
305
333
if self .chunk_size is not None :
306
334
download .chunksize = self .chunk_size
307
335
308
- request = Request (download_url , 'GET' )
336
+ headers = {}
337
+ if encryption_key :
338
+ _set_encryption_headers (encryption_key , headers )
339
+
340
+ request = Request (download_url , 'GET' , headers )
309
341
310
342
# Use the private ``_connection`` rather than the public
311
343
# ``.connection``, since the public connection may be a batch. A
@@ -315,27 +347,36 @@ def download_to_file(self, file_obj, client=None):
315
347
# it has all three (http, API_BASE_URL and build_api_url).
316
348
download .initialize_download (request , client ._connection .http )
317
349
318
- def download_to_filename (self , filename , client = None ):
350
+ def download_to_filename (self , filename , encryption_key = None , client = None ):
319
351
"""Download the contents of this blob into a named file.
320
352
321
353
:type filename: string
322
354
:param filename: A filename to be passed to ``open``.
323
355
356
+ :type encryption_key: str or bytes
357
+ :param encryption_key: Optional 32 byte encryption key for
358
+ customer-supplied encryption.
359
+
324
360
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
325
361
:param client: Optional. The client to use. If not passed, falls back
326
362
to the ``client`` stored on the blob's bucket.
327
363
328
364
:raises: :class:`gcloud.exceptions.NotFound`
329
365
"""
330
366
with open (filename , 'wb' ) as file_obj :
331
- self .download_to_file (file_obj , client = client )
367
+ self .download_to_file (file_obj , encryption_key = encryption_key ,
368
+ client = client )
332
369
333
370
mtime = time .mktime (self .updated .timetuple ())
334
371
os .utime (file_obj .name , (mtime , mtime ))
335
372
336
- def download_as_string (self , client = None ):
373
+ def download_as_string (self , encryption_key = None , client = None ):
337
374
"""Download the contents of this blob as a string.
338
375
376
+ :type encryption_key: str or bytes
377
+ :param encryption_key: Optional 32 byte encryption key for
378
+ customer-supplied encryption.
379
+
339
380
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
340
381
:param client: Optional. The client to use. If not passed, falls back
341
382
to the ``client`` stored on the blob's bucket.
@@ -345,7 +386,8 @@ def download_as_string(self, client=None):
345
386
:raises: :class:`gcloud.exceptions.NotFound`
346
387
"""
347
388
string_buffer = BytesIO ()
348
- self .download_to_file (string_buffer , client = client )
389
+ self .download_to_file (string_buffer , encryption_key = encryption_key ,
390
+ client = client )
349
391
return string_buffer .getvalue ()
350
392
351
393
@staticmethod
@@ -358,8 +400,10 @@ def _check_response_error(request, http_response):
358
400
raise make_exception (faux_response , http_response .content ,
359
401
error_info = request .url )
360
402
403
+ # pylint: disable=too-many-locals
361
404
def upload_from_file (self , file_obj , rewind = False , size = None ,
362
- content_type = None , num_retries = 6 , client = None ):
405
+ encryption_key = None , content_type = None , num_retries = 6 ,
406
+ client = None ):
363
407
"""Upload the contents of this blob from a file-like object.
364
408
365
409
The content type of the upload will either be
@@ -378,6 +422,25 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
378
422
`lifecycle <https://cloud.google.com/storage/docs/lifecycle>`_
379
423
API documents for details.
380
424
425
+ Uploading a file with a `customer-supplied`_ encryption key::
426
+
427
+ >>> from gcloud import storage
428
+ >>> from gcloud.storage import Blob
429
+
430
+ >>> client = storage.Client(project='my-project')
431
+ >>> bucket = client.get_bucket('my-bucket')
432
+ >>> encryption_key = 'aa426195405adee2c8081bb9e7e74b19'
433
+ >>> blob = Blob('secure-data', bucket)
434
+ >>> with open('my-file', 'rb') as my_file:
435
+ >>> blob.upload_from_file(my_file,
436
+ ... encryption_key=encryption_key)
437
+
438
+ The ``encryption_key`` should be a str or bytes with a length of at
439
+ least 32.
440
+
441
+ .. _customer-supplied: https://cloud.google.com/storage/docs/\
442
+ encryption#customer-supplied
443
+
381
444
:type file_obj: file
382
445
:param file_obj: A file handle open for reading.
383
446
@@ -391,6 +454,10 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
391
454
:func:`os.fstat`. (If the file handle is not from the
392
455
filesystem this won't be possible.)
393
456
457
+ :type encryption_key: str or bytes
458
+ :param encryption_key: Optional 32 byte encryption key for
459
+ customer-supplied encryption.
460
+
394
461
:type content_type: string or ``NoneType``
395
462
:param content_type: Optional type of content being uploaded.
396
463
@@ -434,6 +501,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
434
501
'User-Agent' : connection .USER_AGENT ,
435
502
}
436
503
504
+ if encryption_key :
505
+ _set_encryption_headers (encryption_key , headers )
506
+
437
507
upload = Upload (file_obj , content_type , total_bytes ,
438
508
auto_transfer = False )
439
509
@@ -473,9 +543,10 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
473
543
six .string_types ): # pragma: NO COVER Python3
474
544
response_content = response_content .decode ('utf-8' )
475
545
self ._set_properties (json .loads (response_content ))
546
+ # pylint: enable=too-many-locals
476
547
477
548
def upload_from_filename (self , filename , content_type = None ,
478
- client = None ):
549
+ encryption_key = None , client = None ):
479
550
"""Upload this blob's contents from the content of a named file.
480
551
481
552
The content type of the upload will either be
@@ -500,6 +571,10 @@ def upload_from_filename(self, filename, content_type=None,
500
571
:type content_type: string or ``NoneType``
501
572
:param content_type: Optional type of content being uploaded.
502
573
574
+ :type encryption_key: str or bytes
575
+ :param encryption_key: Optional 32 byte encryption key for
576
+ customer-supplied encryption.
577
+
503
578
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
504
579
:param client: Optional. The client to use. If not passed, falls back
505
580
to the ``client`` stored on the blob's bucket.
@@ -510,10 +585,10 @@ def upload_from_filename(self, filename, content_type=None,
510
585
511
586
with open (filename , 'rb' ) as file_obj :
512
587
self .upload_from_file (file_obj , content_type = content_type ,
513
- client = client )
588
+ encryption_key = encryption_key , client = client )
514
589
515
590
def upload_from_string (self , data , content_type = 'text/plain' ,
516
- client = None ):
591
+ encryption_key = None , client = None ):
517
592
"""Upload contents of this blob from the provided string.
518
593
519
F438
594
.. note::
@@ -535,6 +610,10 @@ def upload_from_string(self, data, content_type='text/plain',
535
610
:param content_type: Optional type of content being uploaded. Defaults
536
611
to ``'text/plain'``.
537
612
613
+ :type encryption_key: str or bytes
614
+ :param encryption_key: Optional 32 byte encryption key for
615
+ customer-supplied encryption.
616
+
538
617
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
539
618
:param client: Optional. The client to use. If not passed, falls back
540
619
to the ``client`` stored on the blob's bucket.
@@ -545,7 +624,7 @@ def upload_from_string(self, data, content_type='text/plain',
545
624
string_buffer .write (data )
546
625
self .upload_from_file (file_obj = string_buffer , rewind = True ,
547
626
size = len (data ), content_type = content_type ,
548
- client = client )
627
+ encryption_key = encryption_key , client = client )
549
628
550
629
def make_public (self , client = None ):
551
630
"""Make this blob public giving all users read access.
@@ -838,3 +917,21 @@ def __init__(self, bucket_name, object_name):
838
917
self .query_params = {'name' : object_name }
839
918
self ._bucket_name = bucket_name
840
919
self ._relative_path = ''
920
+
921
+
922
+ def _set_encryption_headers (key , headers ):
923
+ """Builds customer encyrption key headers
924
+
925
+ :type key: str or bytes
926
+ :param key: 32 byte key to build request key and hash.
927
+
928
+ :type headers: dict
929
+ :param headers: dict of HTTP headers being sent in request.
930
+ """
931
+ key = _to_bytes (key )
932
+ sha256_key = hashlib .sha256 (key ).digest ()
933
+ key_hash = base64 .b64encode (sha256_key ).rstrip ()
934
+ encoded_key = base64 .b64encode (key ).rstrip ()
935
+ headers ['X-Goog-Encryption-Algorithm' ] = 'AES256'
936
+ headers ['X-Goog-Encryption-Key' ] = _bytes_to_unicode (encoded_key )
937
+ headers ['X-Goog-Encryption-Key-Sha256' ] = _bytes_to_unicode (key_hash )
0 commit comments