Skip to content

Commit 7bbad7d

Browse files
authored
az storage blob download/download-batch: Migrate to use track2 SDK 2021-04-10 API version (Azure#21711)
1 parent 5399074 commit 7bbad7d

14 files changed

+1063
-604
lines changed

src/azure-cli-core/azure/cli/core/profiles/_shared.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def default_api_version(self):
191191
ResourceType.DATA_KEYVAULT_ADMINISTRATION_BACKUP: '7.2-preview',
192192
ResourceType.DATA_KEYVAULT_ADMINISTRATION_ACCESS_CONTROL: '7.2-preview',
193193
ResourceType.DATA_STORAGE: '2018-11-09',
194-
ResourceType.DATA_STORAGE_BLOB: '2020-10-02',
194+
ResourceType.DATA_STORAGE_BLOB: '2021-04-10',
195195
ResourceType.DATA_STORAGE_FILEDATALAKE: '2020-02-10',
196196
ResourceType.DATA_STORAGE_FILESHARE: '2019-07-07',
197197
ResourceType.DATA_STORAGE_QUEUE: '2018-03-28',

src/azure-cli/azure/cli/command_modules/storage/_help.py

+8
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,14 @@
11841184
az storage blob upload-batch -d mycontainer -s <path-to-directory> --pattern cli-201[!89]-??-??.txt
11851185
"""
11861186

1187+
helps['storage blob download'] = """
1188+
type: command
1189+
short-summary: Download a blob to a file path.
1190+
examples:
1191+
- name: Download a blob.
1192+
text: az storage blob download -f /path/to/file -c mycontainer -n MyBlob
1193+
"""
1194+
11871195
helps['storage blob url'] = """
11881196
type: command
11891197
short-summary: Create the url to access a blob.

src/azure-cli/azure/cli/command_modules/storage/_params.py

+39-11
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
validate_fs_public_access, validate_logging_version, validate_or_policy, validate_policy,
2424
get_api_version_type, blob_download_file_path_validator, blob_tier_validator, validate_subnet,
2525
validate_immutability_arguments, validate_blob_name_for_upload, validate_share_close_handle,
26-
add_upload_progress_callback, blob_tier_validator_track2)
26+
add_upload_progress_callback, blob_tier_validator_track2, add_download_progress_callback)
2727

2828

2929
def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statements, too-many-lines, too-many-branches, line-too-long
@@ -247,6 +247,11 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
247247
public_network_access_enum = self.get_sdk('models._storage_management_client_enums#PublicNetworkAccess',
248248
resource_type=ResourceType.MGMT_STORAGE)
249249

250+
version_id_type = CLIArgumentType(
251+
help='An optional blob version ID. This parameter is only for versioning enabled account. ',
252+
min_api='2019-12-12', is_preview=True
253+
)
254+
250255
with self.argument_context('storage') as c:
251256
c.argument('container_name', container_name_type)
252257
c.argument('directory_name', directory_type)
@@ -986,22 +991,45 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
986991
'blob. If set overwrite=True, then the existing append blob will be deleted, and a new one created. '
987992
'Defaults to False.')
988993

989-
with self.argument_context('storage blob download') as c:
990-
c.argument('file_path', options_list=('--file', '-f'), type=file_type,
991-
completer=FilesCompleter(), validator=blob_download_file_path_validator)
992-
c.argument('max_connections', type=int)
993-
c.argument('start_range', type=int)
994-
c.argument('end_range', type=int)
995-
c.argument('validate_content', action='store_true', min_api='2016-05-31')
996-
c.extra('no_progress', progress_type)
994+
with self.argument_context('storage blob download', resource_type=ResourceType.DATA_STORAGE_BLOB) as c:
995+
c.register_blob_arguments_track2()
996+
c.register_precondition_options()
997+
c.argument('file_path', options_list=('--file', '-f'), type=file_type, completer=FilesCompleter(),
998+
help='Path of file to write out to.', validator=blob_download_file_path_validator)
999+
c.argument('start_range', type=int,
1000+
help='Start of byte range to use for downloading a section of the blob. If no end_range is given, '
1001+
'all bytes after the start_range will be downloaded. The start_range and end_range params are '
1002+
'inclusive. Ex: start_range=0, end_range=511 will download first 512 bytes of blob.')
1003+
c.argument('end_range', type=int,
1004+
help='End of byte range to use for downloading a section of the blob. If end_range is given, '
1005+
'start_range must be provided. The start_range and end_range params are inclusive. '
1006+
'Ex: start_range=0, end_range=511 will download first 512 bytes of blob.')
1007+
c.extra('no_progress', progress_type, validator=add_download_progress_callback)
1008+
c.extra('snapshot', help='The snapshot parameter is an opaque DateTime value that, when present, '
1009+
'specifies the blob snapshot to retrieve.')
1010+
c.extra('lease', options_list=['--lease-id'], help='Required if the blob has an active lease.')
1011+
c.extra('version_id', version_id_type)
1012+
c.extra('max_concurrency', options_list=['--max-connections'], type=int, default=2,
1013+
help='The number of parallel connections with which to download.')
1014+
c.argument('open_mode', help='Mode to use when opening the file. Note that specifying append only open_mode '
1015+
'prevents parallel download. So, max_connections must be set to 1 '
1016+
'if this open_mode is used.')
1017+
c.extra('validate_content', action='store_true', min_api='2016-05-31',
1018+
help='If true, calculates an MD5 hash for each chunk of the blob. The storage service checks the '
1019+
'hash of the content that has arrived with the hash that was sent. This is primarily valuable for '
1020+
'detecting bitflips on the wire if using http instead of https, as https (the default), '
1021+
'will already validate. Note that this MD5 hash is not stored with the blob. Also note that '
1022+
'if enabled, the memory-efficient algorithm will not be used because computing the MD5 hash '
1023+
'requires buffering entire blocks, and doing so defeats the purpose of the memory-efficient '
1024+
'algorithm.')
9971025

9981026
with self.argument_context('storage blob download-batch') as c:
9991027
c.ignore('source_container_name')
10001028
c.argument('destination', options_list=('--destination', '-d'))
10011029
c.argument('source', options_list=('--source', '-s'))
10021030
c.extra('no_progress', progress_type)
1003-
c.argument('max_connections', type=int,
1004-
help='Maximum number of parallel connections to use when the blob size exceeds 64MB.')
1031+
c.extra('max_concurrency', options_list=['--max-connections'], type=int, default=2,
1032+
help='The number of parallel connections with which to download.')
10051033

10061034
with self.argument_context('storage blob delete') as c:
10071035
from .sdkutil import get_delete_blob_snapshot_type_names

src/azure-cli/azure/cli/command_modules/storage/_validators.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1025,15 +1025,16 @@ def process_container_delete_parameters(cmd, namespace):
10251025

10261026
def process_blob_download_batch_parameters(cmd, namespace):
10271027
"""Process the parameters for storage blob download command"""
1028+
from azure.cli.core.azclierror import InvalidArgumentValueError
10281029
# 1. quick check
10291030
if not os.path.exists(namespace.destination) or not os.path.isdir(namespace.destination):
1030-
raise ValueError('incorrect usage: destination must be an existing directory')
1031+
raise InvalidArgumentValueError('incorrect usage: destination must be an existing directory')
10311032

10321033
# 2. try to extract account name and container name from source string
10331034
_process_blob_batch_container_parameters(cmd, namespace)
10341035

10351036
# 3. Call validators
1036-
add_progress_callback(cmd, namespace)
1037+
add_download_progress_callback(cmd, namespace)
10371038

10381039

10391040
def process_blob_upload_batch_parameters(cmd, namespace):
@@ -2033,6 +2034,29 @@ def _update_progress(response):
20332034
del namespace.no_progress
20342035

20352036

2037+
def add_download_progress_callback(cmd, namespace):
2038+
def _update_progress(response):
2039+
if response.http_response.status_code not in [200, 201, 206]:
2040+
return
2041+
2042+
message = getattr(_update_progress, 'message', 'Alive')
2043+
reuse = getattr(_update_progress, 'reuse', False)
2044+
current = response.context['download_stream_current']
2045+
total = response.context['data_stream_total']
2046+
2047+
if total:
2048+
hook.add(message=message, value=current, total_val=total)
2049+
if total == current and not reuse:
2050+
hook.end()
2051+
2052+
hook = cmd.cli_ctx.get_progress_controller(det=True)
2053+
_update_progress.hook = hook
2054+
2055+
if not namespace.no_progress:
2056+
namespace.progress_callback = _update_progress
2057+
del namespace.no_progress
2058+
2059+
20362060
def validate_blob_arguments(namespace):
20372061
from azure.cli.core.azclierror import RequiredArgumentMissingError
20382062
if not namespace.blob_url and not all([namespace.blob_name, namespace.container_name]):

src/azure-cli/azure/cli/command_modules/storage/commands.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def get_custom_sdk(custom_module, client_factory, resource_type=ResourceType.DAT
317317
from ._transformers import transform_blob_list_output, transform_blob_json_output, transform_blob_upload_output
318318
from ._format import transform_blob_output
319319
from ._exception_handler import file_related_exception_handler
320-
from ._validators import process_blob_upload_batch_parameters
320+
from ._validators import process_blob_upload_batch_parameters, process_blob_download_batch_parameters
321321
g.storage_custom_command_oauth('copy start', 'copy_blob')
322322
g.storage_custom_command_oauth('show', 'show_blob_v2', transform=transform_blob_json_output,
323323
table_transformer=transform_blob_output,
@@ -337,6 +337,13 @@ def get_custom_sdk(custom_module, client_factory, resource_type=ResourceType.DAT
337337
g.storage_custom_command_oauth('upload-batch', 'storage_blob_upload_batch', client_factory=cf_blob_service,
338338
validator=process_blob_upload_batch_parameters,
339339
exception_handler=file_related_exception_handler)
340+
g.storage_custom_command_oauth('download', 'download_blob',
341+
transform=transform_blob_json_output,
342+
table_transformer=transform_blob_output,
343+
exception_handler=file_related_exception_handler)
344+
g.storage_custom_command_oauth('download-batch', 'storage_blob_download_batch', client_factory=cf_blob_service,
345+
validator=process_blob_download_batch_parameters,
346+
exception_handler=file_related_exception_handler)
340347

341348
blob_lease_client_sdk = CliCommandType(
342349
operations_tmpl='azure.multiapi.storagev2.blob._lease#BlobLeaseClient.{}',
@@ -359,11 +366,11 @@ def get_custom_sdk(custom_module, client_factory, resource_type=ResourceType.DAT
359366
from ._format import transform_boolean_for_table, transform_blob_output
360367
from ._transformers import (transform_storage_list_output, transform_url,
361368
create_boolean_result_output_transformer)
362-
from ._validators import (process_blob_download_batch_parameters, process_blob_delete_batch_parameters)
369+
from ._validators import process_blob_delete_batch_parameters
363370
from ._exception_handler import file_related_exception_handler
364-
g.storage_command_oauth(
365-
'download', 'get_blob_to_path', table_transformer=transform_blob_output,
366-
exception_handler=file_related_exception_handler)
371+
# g.storage_command_oauth(
372+
# 'download', 'get_blob_to_path', table_transformer=transform_blob_output,
373+
# exception_handler=file_related_exception_handler)
367374
g.storage_custom_command_oauth('generate-sas', 'generate_sas_blob_uri')
368375
g.storage_custom_command_oauth(
369376
'url', 'create_blob_url', transform=transform_url)
@@ -376,9 +383,9 @@ def get_custom_sdk(custom_module, client_factory, resource_type=ResourceType.DAT
376383
transform=create_boolean_result_output_transformer(
377384
'undeleted'),
378385
table_transformer=transform_boolean_for_table, min_api='2017-07-29')
379-
g.storage_custom_command_oauth('download-batch', 'storage_blob_download_batch',
380-
validator=process_blob_download_batch_parameters,
381-
exception_handler=file_related_exception_handler)
386+
# g.storage_custom_command_oauth('download-batch', 'storage_blob_download_batch',
387+
# validator=process_blob_download_batch_parameters,
388+
# exception_handler=file_related_exception_handler)
382389
g.storage_custom_command_oauth('delete-batch', 'storage_blob_delete_batch',
383390
validator=process_blob_delete_batch_parameters)
384391
g.storage_command_oauth(

src/azure-cli/azure/cli/command_modules/storage/operations/blob.py

+39-13
Original file line numberDiff line numberDiff line change
@@ -354,17 +354,10 @@ def action_file_copy(file_info):
354354

355355
# pylint: disable=unused-argument
356356
def storage_blob_download_batch(client, source, destination, source_container_name, pattern=None, dryrun=False,
357-
progress_callback=None, max_connections=2):
358-
359-
def _download_blob(blob_service, container, destination_folder, normalized_blob_name, blob_name):
360-
# TODO: try catch IO exception
361-
destination_path = os.path.join(destination_folder, normalized_blob_name)
362-
destination_folder = os.path.dirname(destination_path)
363-
if not os.path.exists(destination_folder):
364-
mkdir_p(destination_folder)
365-
366-
blob = blob_service.get_blob_to_path(container, blob_name, destination_path, max_connections=max_connections,
367-
progress_callback=progress_callback)
357+
progress_callback=None, **kwargs):
358+
@check_precondition_success
359+
def _download_blob(*args, **kwargs):
360+
blob = download_blob(*args, **kwargs)
368361
return blob.name
369362

370363
source_blobs = collect_blobs(client, source_container_name, pattern)
@@ -394,17 +387,34 @@ def _download_blob(blob_service, container, destination_folder, normalized_blob_
394387

395388
results = []
396389
for index, blob_normed in enumerate(blobs_to_download):
390+
from azure.cli.core.azclierror import FileOperationError
397391
# add blob name and number to progress message
398392
if progress_callback:
399393
progress_callback.message = '{}/{}: "{}"'.format(
400394
index + 1, len(blobs_to_download), blobs_to_download[blob_normed])
401-
results.append(_download_blob(
402-
client, source_container_name, destination, blob_normed, blobs_to_download[blob_normed]))
395+
blob_client = client.get_blob_client(container=source_container_name,
396+
blob=blobs_to_download[blob_normed])
397+
destination_path = os.path.join(destination, os.path.normpath(blob_normed))
398+
destination_folder = os.path.dirname(destination_path)
399+
# Failed when there is same name for file and folder
400+
if os.path.isfile(destination_path) and os.path.exists(destination_folder):
401+
raise FileOperationError("%s already exists in %s. Please rename existing file or choose another "
402+
"destination folder. ")
403+
if not os.path.exists(destination_folder):
404+
mkdir_p(destination_folder)
405+
include, result = _download_blob(client=blob_client, file_path=destination_path,
406+
progress_callback=progress_callback, **kwargs)
407+
if include:
408+
results.append(result)
403409

404410
# end progress hook
405411
if progress_callback:
406412
progress_callback.hook.end()
407413

414+
num_failures = len(blobs_to_download) - len(results)
415+
if num_failures:
416+
logger.warning('%s of %s files not downloaded due to "Failed Precondition"',
417+
num_failures, len(blobs_to_download))
408418
return results
409419

410420

@@ -589,6 +599,22 @@ def upload_blob(cmd, client, file_path=None, container_name=None, blob_name=None
589599
return response
590600

591601

602+
def download_blob(client, file_path, open_mode='wb', start_range=None, end_range=None,
603+
progress_callback=None, **kwargs):
604+
offset = None
605+
length = None
606+
if start_range is not None and end_range is not None:
607+
offset = start_range
608+
length = end_range - start_range + 1
609+
if progress_callback:
610+
kwargs['raw_response_hook'] = progress_callback
611+
download_stream = client.download_blob(offset=offset, length=length, **kwargs)
612+
with open(file_path, open_mode) as stream:
613+
download_stream.readinto(stream)
614+
615+
return download_stream.properties
616+
617+
592618
def get_block_ids(content_length, block_length):
593619
"""Get the block id arrary from block blob length, block size"""
594620
block_count = 0

src/azure-cli/azure/cli/command_modules/storage/tests/latest/recordings/test_storage_blob_upload_midsize_file.yaml

+560-320
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)