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
@@ -109,6 +111,23 @@ def path_helper(bucket_path, blob_name):
109
111
"""
110
112
return bucket_path + '/o/' + quote (blob_name , safe = '' )
111
113
114<
10000
/code>
+ @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
+
112
131
@property
113
132
def acl (self ):
114
133
"""Create our ACL on demand."""
@@ -276,17 +295,34 @@ def delete(self, client=None):
276
295
"""
277
296
return self .bucket .delete_blob (self .name , client = client )
278
297
279
- def download_to_file (self , file_obj , client = None ):
298
+ def download_to_file (self , file_obj , key = None , client = None ):
280
299
"""Download the contents of this blob into a file-like object.
281
300
282
301
.. note::
283
302
284
303
If the server-set property, :attr:`media_link`, is not yet
285
304
initialized, makes an additional API request to load it.
286
305
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&
10000
#39;, 'w') as file_obj:
318
+ >>> blob.download_to_file(file_obj, key=key)
319
+
287
320
:type file_obj: file
288
321
:param file_obj: A file handle to which to write the blob's data.
289
322
323
+ :type key: str
324
+ :param key: Optional 32 byte key for customer-supplied encryption.
325
+
290
326
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
291
327
:param client: Optional. The client to use. If not passed, falls back
292
328
to the ``client`` stored on the blob's bucket.
@@ -305,7 +341,11 @@ def download_to_file(self, file_obj, client=None):
305
341
if self .chunk_size is not None :
306
342
download .chunksize = self .chunk_size
307
343
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 )
309
349
310
350
# Use the private ``_connection`` rather than the public
311
351
# ``.connection``, since the public connection may be a batch. A
@@ -315,27 +355,46 @@ def download_to_file(self, file_obj, client=None):
315
355
# it has all three (http, API_BASE_URL and build_api_url).
316
356
download .initialize_download (request , client ._connection .http )
317
357
318
- def download_to_filename (self , filename , client = None ):
358
+ def download_to_filename (self , filename , key = None , client = None ):
319
359
"""Download the contents of this blob into a named file.
320
360
321
361
:type filename: string
322
362
:param filename: A filename to be passed to ``open``.
323
363
364
+ :type key: str
365
+ :param key: Optional 32 byte key for customer-supplied encryption.
366
+
324
367
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
325
368
:param client: Optional. The client to use. If not passed, falls back
326
369
to the ``client`` stored on the blob's bucket.
327
370
328
371
:raises: :class:`gcloud.exceptions.NotFound`
329
372
"""
330
373
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 )
332
375
333
376
mtime = time .mktime (self .updated .timetuple ())
334
377
os .utime (file_obj .name , (mtime , mtime ))
335
378
336
- def download_as_string (self , client = None ):
379
+ def download_as_string (self , key = None , client = None ):
337
380
"""Download the contents of this blob as a string.
338
381
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
+
339
398
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
340
399
:param client: Optional. The client to use. If not passed, falls back
341
400
to the ``client`` stored on the blob's bucket.
@@ -345,7 +404,7 @@ def download_as_string(self, client=None):
345
404
:raises: :class:`gcloud.exceptions.NotFound`
346
405
"""
347
406
string_buffer = BytesIO ()
348
- self .download_to_file (string_buffer , client = client )
407
+ self .download_to_file (string_buffer , key = key , client = client )
349
408
return string_buffer .getvalue ()
350
409
351
410
@staticmethod
@@ -358,7 +417,8 @@ def _check_response_error(request, http_response):
358
417
raise make_exception (faux_response , http_response .content ,
359
418
error_info = request .url )
360
419
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 ,
362
422
content_type = None , num_retries = 6 , client = None ):
363
423
"""Upload the contents of this blob from a file-like object.
364
424
@@ -391,6 +451,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
391
451
:func:`os.fstat`. (If the file handle is not from the
392
452
filesystem this won't be possible.)
393
453
454
+ :type key: str
455
+ :param key: Optional 32 byte key for customer-supplied encryption.
456
+
394
457
:type content_type: string or ``NoneType``
395
458
:param content_type: Optional type of content being uploaded.
396
459
@@ -434,6 +497,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
434
497
'User-Agent' : connection .USER_AGENT ,
435
498
}
436
499
500
+ if key :
501
+ headers .update (self ._get_customer_encryption_headers (key ))
502
+
437
503
upload = Upload (file_obj , content_type , total_bytes ,
438
504
auto_transfer = False )
439
505
@@ -474,7 +540,7 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
474
540
response_content = response_content .decode ('utf-8' )
475
541
self ._set_properties (json .loads (response_content ))
476
542
477
- def upload_from_filename (self , filename , content_type = None ,
543
+ def upload_from_filename (self , filename , content_type = None , key = None ,
478
544
client = None ):
479
545
"""Upload this blob's contents from the content of a named file.
480
546
@@ -500,6 +566,9 @@ def upload_from_filename(self, filename, content_type=None,
500
566
:type content_type: string or ``NoneType``
501
567
:param content_type: Optional type of content being uploaded.
502
568
569
+ :type key: str
570
+ :param key: Optional 32 byte key for customer-supplied encryption.
571
+
503
572
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
504
573
:param client: Optional. The client to use. If not passed, falls back
505
574
to the ``client`` stored on the blob's bucket.
@@ -509,10 +578,10 @@ def upload_from_filename(self, filename, content_type=None,
509
578
content_type , _ = mimetypes .guess_type (filename )
510
579
511
580
with
10000
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 ,
513
582
client = client )
514
583
515
- def upload_from_string (self , data , content_type = 'text/plain' ,
584
+ def upload_from_string (self , data , content_type = 'text/plain' , key = None ,
516
585
client = None ):
517
586
"""Upload contents of this blob from the provided string.
518
587
@@ -527,6 +596,19 @@ def upload_from_string(self, data, content_type='text/plain',
527
596
`lifecycle <https://cloud.google.com/storage/docs/lifecycle>`_
528
597
API documents for details.
529
598
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
+
530
612
:type data: bytes or text
531
613
:param data: The data to store in this blob. If the value is
532
614
text, it will be encoded as UTF-8.
@@ -535,6 +617,9 @@ def upload_from_string(self, data, content_type='text/plain',
535
617
:param content_type: Optional type of content being uploaded. Defaults
536
618
to ``'text/plain'``.
537
619
620
+ :type key: str
621
+ :param key: Optional 32 byte key for customer-supplied encryption.
622
+
538
623
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
539
624
:param client: Optional. The client to use. If not passed, falls back
540
625
to the ``client`` stored on the blob's bucket.
@@ -545,7 +630,7 @@ def upload_from_string(self, data, content_type='text/plain',
545
630
string_buffer .write (data )
546
631
self .upload_from_file (file_obj = string_buffer , rewind = True ,
547
632
size = len (data ), content_type = content_type ,
548
- client = client )
633
+ key = key , client = client )
549
634
550
635
def make_public (self , client = None ):
551
636
"""Make this blob public giving all users read access.
0 commit comments