From 347c6ee0475928ac10df9ef7b2abe6c0468ffbc1 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 14:23:01 +0000 Subject: [PATCH 01/10] feat(storage): support returning skipped items as UserWarning in download_many_to_path --- google/cloud/storage/transfer_manager.py | 33 ++++++--- tests/system/test_transfer_manager.py | 86 +++++++++++++++++++++++- tests/unit/test_transfer_manager.py | 84 ++++++++++++++++++++--- 3 files changed, 184 insertions(+), 19 deletions(-) diff --git a/google/cloud/storage/transfer_manager.py b/google/cloud/storage/transfer_manager.py index c655244b0..91119cc1d 100644 --- a/google/cloud/storage/transfer_manager.py +++ b/google/cloud/storage/transfer_manager.py @@ -805,43 +805,60 @@ def download_many_to_path( :raises: :exc:`concurrent.futures.TimeoutError` if deadline is exceeded. - :rtype: list + :rtype: List[None|Exception|UserWarning] :returns: A list of results corresponding to, in order, each item in the - input list. If an exception was received, it will be the result - for that operation. Otherwise, the return value from the successful - download method is used (which will be None). + input list. If an exception was received or a download was skipped + (e.g., due to existing file or path traversal), it will be the result + for that operation (as an Exception or UserWarning, respectively). + Otherwise, the result will be None for a successful download. """ + results = [None] * len(blob_names) blob_file_pairs = [] + indices_to_process = [] - for blob_name in blob_names: + for i, blob_name in enumerate(blob_names): full_blob_name = blob_name_prefix + blob_name resolved_path = _resolve_path(destination_directory, blob_name) if not resolved_path.parent.is_relative_to( Path(destination_directory).resolve() ): - warnings.warn( + msg = ( f"The blob {blob_name} will **NOT** be downloaded. " f"The resolved destination_directory - {resolved_path.parent} - is either invalid or " f"escapes user provided {Path(destination_directory).resolve()} . Please download this file separately using `download_to_filename`" ) + warnings.warn(msg) + results[i] = UserWarning(msg) continue resolved_path = str(resolved_path) + if skip_if_exists and os.path.isfile(resolved_path): + msg = f"The blob {blob_name} is skipped because destination file already exists" + results[i] = UserWarning(msg) + continue + if create_directories: directory, _ = os.path.split(resolved_path) os.makedirs(directory, exist_ok=True) blob_file_pairs.append((bucket.blob(full_blob_name), resolved_path)) + indices_to_process.append(i) - return download_many( + many_results = download_many( blob_file_pairs, download_kwargs=download_kwargs, deadline=deadline, raise_exception=raise_exception, worker_type=worker_type, max_workers=max_workers, - skip_if_exists=skip_if_exists, + skip_if_exists=False, ) + for meta_index, result in zip(indices_to_process, many_results): + results[meta_index] = result + + return results + + def download_chunks_concurrently( blob, diff --git a/tests/system/test_transfer_manager.py b/tests/system/test_transfer_manager.py index 844562c90..6bb0e03fd 100644 --- a/tests/system/test_transfer_manager.py +++ b/tests/system/test_transfer_manager.py @@ -187,8 +187,9 @@ def test_download_many_to_path_skips_download( [str(warning.message) for warning in w] ) - # 1 total - 1 skipped = 0 results - assert len(results) == 0 + # 1 total - 1 skipped = 1 result (containing Warning) + assert len(results) == 1 + assert isinstance(results[0], UserWarning) @pytest.mark.parametrize( @@ -266,6 +267,87 @@ def test_download_many_to_path_downloads_within_dest_dir( assert downloaded_contents == source_contents + +def test_download_many_to_path_mixed_results( + shared_bucket, file_data, blobs_to_delete +): + """ + Test download_many_to_path with successful downloads, skip_if_exists skips, and path traversal skips. + """ + PREFIX = "mixed_results/" + BLOBNAMES = [ + "success1.txt", + "success2.txt", + "exists.txt", + "../escape.txt" + ] + + FILE_BLOB_PAIRS = [ + ( + file_data["simple"]["path"], + shared_bucket.blob(PREFIX + name), + ) + for name in BLOBNAMES + ] + + results = transfer_manager.upload_many( + FILE_BLOB_PAIRS, + skip_if_exists=True, + deadline=DEADLINE, + ) + for result in results: + assert result is None + + blobs = list(shared_bucket.list_blobs(prefix=PREFIX)) + blobs_to_delete.extend(blobs) + assert len(blobs) == 4 + + # Actual Test + with tempfile.TemporaryDirectory() as tempdir: + existing_file_path = os.path.join(tempdir, "exists.txt") + with open(existing_file_path, "w") as f: + f.write("already here") + + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + results = transfer_manager.download_many_to_path( + shared_bucket, + BLOBNAMES, + destination_directory=tempdir, + blob_name_prefix=PREFIX, + deadline=DEADLINE, + create_directories=True, + skip_if_exists=True, + ) + + assert len(results) == 4 + + path_traversal_warnings = [ + warning + for warning in w + if str(warning.message).startswith("The blob ") + and "will **NOT** be downloaded. The resolved destination_directory" + in str(warning.message) + ] + assert len(path_traversal_warnings) == 1, "---".join( + [str(warning.message) for warning in w] + ) + + assert results[0] is None + assert results[1] is None + assert isinstance(results[2], UserWarning) + assert "skipped because destination file already exists" in str(results[2]) + assert isinstance(results[3], UserWarning) + assert "will **NOT** be downloaded" in str(results[3]) + + assert os.path.exists(os.path.join(tempdir, "success1.txt")) + assert os.path.exists(os.path.join(tempdir, "success2.txt")) + + with open(existing_file_path, "r") as f: + assert f.read() == "already here" + + def test_download_many(listable_bucket): blobs = list(listable_bucket.list_blobs()) with tempfile.TemporaryDirectory() as tempdir: diff --git a/tests/unit/test_transfer_manager.py b/tests/unit/test_transfer_manager.py index 85ffd9eaa..799bfd314 100644 --- a/tests/unit/test_transfer_manager.py +++ b/tests/unit/test_transfer_manager.py @@ -530,9 +530,10 @@ def test_download_many_to_path(): ] with mock.patch( - "google.cloud.storage.transfer_manager.download_many" + "google.cloud.storage.transfer_manager.download_many", + return_value=[FAKE_RESULT] * len(BLOBNAMES), ) as mock_download_many: - transfer_manager.download_many_to_path( + results = transfer_manager.download_many_to_path( bucket, BLOBNAMES, destination_directory=PATH_ROOT, @@ -553,11 +554,71 @@ def test_download_many_to_path(): raise_exception=True, max_workers=MAX_WORKERS, worker_type=WORKER_TYPE, - skip_if_exists=True, + skip_if_exists=False, ) + assert results == [FAKE_RESULT] * len(BLOBNAMES) for blobname in BLOBNAMES: bucket.blob.assert_any_call(BLOB_NAME_PREFIX + blobname) +def test_download_many_to_path_with_skip_if_exists(): + bucket = mock.Mock() + + BLOBNAMES = ["file_a.txt", "file_b.txt", "dir_a/file_c.txt"] + PATH_ROOT = "mypath/" + BLOB_NAME_PREFIX = "myprefix/" + DOWNLOAD_KWARGS = {"accept-encoding": "fake-gzip"} + MAX_WORKERS = 7 + DEADLINE = 10 + WORKER_TYPE = transfer_manager.THREAD + + from google.cloud.storage.transfer_manager import _resolve_path + + existing_file = str(_resolve_path(PATH_ROOT, "file_a.txt")) + + def isfile_side_effect(path): + return path == existing_file + + EXPECTED_BLOB_FILE_PAIRS = [ + (mock.ANY, str(_resolve_path(PATH_ROOT, "file_b.txt"))), + (mock.ANY, str(_resolve_path(PATH_ROOT, "dir_a/file_c.txt"))), + ] + + with mock.patch("os.path.isfile", side_effect=isfile_side_effect): + with mock.patch( + "google.cloud.storage.transfer_manager.download_many", + return_value=[FAKE_RESULT, FAKE_RESULT], + ) as mock_download_many: + results = transfer_manager.download_many_to_path( + bucket, + BLOBNAMES, + destination_directory=PATH_ROOT, + blob_name_prefix=BLOB_NAME_PREFIX, + download_kwargs=DOWNLOAD_KWARGS, + deadline=DEADLINE, + create_directories=False, + raise_exception=True, + max_workers=MAX_WORKERS, + worker_type=WORKER_TYPE, + skip_if_exists=True, + ) + + mock_download_many.assert_called_once_with( + EXPECTED_BLOB_FILE_PAIRS, + download_kwargs=DOWNLOAD_KWARGS, + deadline=DEADLINE, + raise_exception=True, + max_workers=MAX_WORKERS, + worker_type=WORKER_TYPE, + skip_if_exists=False, + ) + + assert len(results) == 3 + assert isinstance(results[0], UserWarning) + assert str(results[0]) == "The blob file_a.txt is skipped because destination file already exists" + assert results[1] == FAKE_RESULT + assert results[2] == FAKE_RESULT + + @pytest.mark.parametrize( "blobname", @@ -584,9 +645,10 @@ def test_download_many_to_path_skips_download(blobname): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") with mock.patch( - "google.cloud.storage.transfer_manager.download_many" + "google.cloud.storage.transfer_manager.download_many", + return_value=[], ) as mock_download_many: - transfer_manager.download_many_to_path( + results = transfer_manager.download_many_to_path( bucket, BLOBNAMES, destination_directory=PATH_ROOT, @@ -614,8 +676,10 @@ def test_download_many_to_path_skips_download(blobname): raise_exception=True, max_workers=MAX_WORKERS, worker_type=WORKER_TYPE, - skip_if_exists=True, + skip_if_exists=False, ) + assert len(results) == 1 + assert isinstance(results[0], UserWarning) @pytest.mark.parametrize( @@ -649,9 +713,10 @@ def test_download_many_to_path_downloads_within_dest_dir(blobname): ] with mock.patch( - "google.cloud.storage.transfer_manager.download_many" + "google.cloud.storage.transfer_manager.download_many", + return_value=[FAKE_RESULT], ) as mock_download_many: - transfer_manager.download_many_to_path( + results = transfer_manager.download_many_to_path( bucket, BLOBNAMES, destination_directory=PATH_ROOT, @@ -672,8 +737,9 @@ def test_download_many_to_path_downloads_within_dest_dir(blobname): raise_exception=True, max_workers=MAX_WORKERS, worker_type=WORKER_TYPE, - skip_if_exists=True, + skip_if_exists=False, ) + assert results == [FAKE_RESULT] bucket.blob.assert_any_call(BLOB_NAME_PREFIX + blobname) From 948de5f9815d277052471d7de96a61c55750c9cf Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 16:01:19 +0000 Subject: [PATCH 02/10] feat(storage): raise InvalidPathError for Windows full paths with ':' in download_many_to_path --- google/cloud/storage/exceptions.py | 6 +++ google/cloud/storage/transfer_manager.py | 13 ++++-- tests/unit/test_transfer_manager.py | 58 ++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/google/cloud/storage/exceptions.py b/google/cloud/storage/exceptions.py index 4eb05cef7..12f69071b 100644 --- a/google/cloud/storage/exceptions.py +++ b/google/cloud/storage/exceptions.py @@ -33,6 +33,12 @@ DataCorruptionDynamicParent = Exception +class InvalidPathError(Exception): + """Raised when the provided path string is malformed.""" + + pass + + class InvalidResponse(InvalidResponseDynamicParent): """Error class for responses which are not in the correct state. diff --git a/google/cloud/storage/transfer_manager.py b/google/cloud/storage/transfer_manager.py index 91119cc1d..69902bdf7 100644 --- a/google/cloud/storage/transfer_manager.py +++ b/google/cloud/storage/transfer_manager.py @@ -39,7 +39,7 @@ from google.cloud.storage._media.requests.upload import XMLMPUContainer from google.cloud.storage._media.requests.upload import XMLMPUPart -from google.cloud.storage.exceptions import DataCorruption +from google.cloud.storage.exceptions import DataCorruption, InvalidPathError TM_DEFAULT_CHUNK_SIZE = 32 * 1024 * 1024 DEFAULT_MAX_WORKERS = 8 @@ -263,6 +263,8 @@ def upload_many( def _resolve_path(target_dir, blob_path): + if os.name == "nt" and ":" in blob_path: + raise InvalidPathError(f"{blob_path} cannot be downloaded into {target_dir}") target_dir = Path(target_dir) blob_path = Path(blob_path) # blob_path.anchor will be '/' if `blob_path` is full path else it'll empty. @@ -818,7 +820,13 @@ def download_many_to_path( for i, blob_name in enumerate(blob_names): full_blob_name = blob_name_prefix + blob_name - resolved_path = _resolve_path(destination_directory, blob_name) + try: + resolved_path = _resolve_path(destination_directory, blob_name) + except InvalidPathError as e: + msg = f"The blob {blob_name} will **NOT** be downloaded. {e}" + warnings.warn(msg) + results[i] = UserWarning(msg) + continue if not resolved_path.parent.is_relative_to( Path(destination_directory).resolve() ): @@ -859,7 +867,6 @@ def download_many_to_path( return results - def download_chunks_concurrently( blob, filename, diff --git a/tests/unit/test_transfer_manager.py b/tests/unit/test_transfer_manager.py index 799bfd314..091716371 100644 --- a/tests/unit/test_transfer_manager.py +++ b/tests/unit/test_transfer_manager.py @@ -513,6 +513,64 @@ def test_upload_many_from_filenames_additional_properties(): assert getattr(blob, attrib) == value + +def test__resolve_path_raises_invalid_path_error_on_windows(): + from google.cloud.storage.transfer_manager import _resolve_path, InvalidPathError + + with mock.patch("os.name", "nt"): + with pytest.raises(InvalidPathError) as exc_info: + _resolve_path("C:\\target", "C:\\target\\file.txt") + assert "cannot be downloaded into" in str(exc_info.value) + + # Test that it DOES NOT raise on non-windows + with mock.patch("os.name", "posix"): + # Should not raise + _resolve_path("/target", "C:\\target\\file.txt") + + +def test_download_many_to_path_raises_invalid_path_error(): + bucket = mock.Mock() + + BLOBNAMES = ["C:\\target\\file.txt"] + PATH_ROOT = "mypath/" + BLOB_NAME_PREFIX = "myprefix/" + DOWNLOAD_KWARGS = {"accept-encoding": "fake-gzip"} + MAX_WORKERS = 7 + DEADLINE = 10 + WORKER_TYPE = transfer_manager.THREAD + + from google.cloud.storage.transfer_manager import InvalidPathError + + def resolve_path_side_effect(target_dir, blob_path): + raise InvalidPathError(f"{blob_path} cannot be downloaded into {target_dir}") + + with mock.patch( + "google.cloud.storage.transfer_manager._resolve_path", + side_effect=resolve_path_side_effect, + ): + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + results = transfer_manager.download_many_to_path( + bucket, + BLOBNAMES, + destination_directory=PATH_ROOT, + blob_name_prefix=BLOB_NAME_PREFIX, + download_kwargs=DOWNLOAD_KWARGS, + deadline=DEADLINE, + create_directories=False, + raise_exception=True, + max_workers=MAX_WORKERS, + worker_type=WORKER_TYPE, + skip_if_exists=True, + ) + + assert len(w) == 1 + assert "will **NOT** be downloaded" in str(w[0].message) + assert len(results) == 1 + assert isinstance(results[0], UserWarning) + + def test_download_many_to_path(): bucket = mock.Mock() From 0db308d586cc703fde5ed9dfefe193bf7c37c961 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 16:10:31 +0000 Subject: [PATCH 03/10] test(storage): update InvalidPathError test to use native triggers --- tests/unit/test_transfer_manager.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_transfer_manager.py b/tests/unit/test_transfer_manager.py index 091716371..90c5c478a 100644 --- a/tests/unit/test_transfer_manager.py +++ b/tests/unit/test_transfer_manager.py @@ -539,16 +539,9 @@ def test_download_many_to_path_raises_invalid_path_error(): DEADLINE = 10 WORKER_TYPE = transfer_manager.THREAD - from google.cloud.storage.transfer_manager import InvalidPathError - - def resolve_path_side_effect(target_dir, blob_path): - raise InvalidPathError(f"{blob_path} cannot be downloaded into {target_dir}") - - with mock.patch( - "google.cloud.storage.transfer_manager._resolve_path", - side_effect=resolve_path_side_effect, - ): + with mock.patch("os.name", "nt"): import warnings + with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") results = transfer_manager.download_many_to_path( From 2c06fbea44da25aa4fafed6284b842b74eae4ea4 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 16:27:02 +0000 Subject: [PATCH 04/10] feat(samples): add argparse and clarify traversal support in download_many snippet --- .../storage_transfer_manager_download_many.py | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index 02cb9b887..b6444f8c5 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -36,11 +36,11 @@ def download_many_blobs_with_transfer_manager( # blob_names = ["myblob", "myblob2"] # The directory on your computer to which to download all of the files. This - # string is prepended (with os.path.join()) to the name of each blob to form - # the full path. Relative paths and absolute paths are both accepted. An - # empty string means "the current working directory". Note that this - # parameter allows accepts directory traversal ("../" etc.) and is not - # intended for unsanitized end user input. + # string is prepended to the name of each blob to form the full path using + # pathlib. Relative paths and absolute paths are both accepted. An empty + # string means "the current working directory". Note that this parameter + # will NOT allow files to escape the destination_directory and will skip + # downloads that attempt directory traversal outside of it. # destination_directory = "" # The maximum number of processes to use for the operation. The performance @@ -60,11 +60,44 @@ def download_many_blobs_with_transfer_manager( ) for name, result in zip(blob_names, results): - # The results list is either `None` or an exception for each blob in + # The results list is either `None`, an exception, or a warning for each blob in # the input list, in order. if isinstance(result, Exception): print("Failed to download {} due to exception: {}".format(name, result)) + elif isinstance(result, Warning): + print("Skipped download for {} due to warning: {}".format(name, result)) else: print("Downloaded {} to {}.".format(name, destination_directory + name)) # [END storage_transfer_manager_download_many] + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Download blobs in a list by name, concurrently in a process pool." + ) + parser.add_argument("bucket_name", help="The ID of your GCS bucket") + parser.add_argument( + "--blobs", + nargs="+", + required=True, + help="The list of blob names to download", + ) + parser.add_argument( + "--destination_directory", + default="", + help="The directory on your computer to which to download all of the files", + ) + parser.add_argument( + "--workers", type=int, default=8, help="The maximum number of processes to use" + ) + + args = parser.parse_args() + + download_many_blobs_with_transfer_manager( + bucket_name=args.bucket_name, + blob_names=args.blobs, + destination_directory=args.destination_directory, + workers=args.workers, + ) From e6c7b54968cd9beb7d7928deac2f34f507af1e74 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 16:32:27 +0000 Subject: [PATCH 05/10] add hypens in arg --- samples/snippets/storage_transfer_manager_download_many.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index b6444f8c5..55611ca29 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -77,7 +77,7 @@ def download_many_blobs_with_transfer_manager( parser = argparse.ArgumentParser( description="Download blobs in a list by name, concurrently in a process pool." ) - parser.add_argument("bucket_name", help="The ID of your GCS bucket") + parser.add_argument("--bucket_name", help="The ID of your GCS bucket") parser.add_argument( "--blobs", nargs="+", From d09c408c7dfbbb3cb3e50904a438e30efc84284a Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 16:45:17 +0000 Subject: [PATCH 06/10] feat(samples): make bucket_name required and clarify description in download_many snippet --- .../storage_transfer_manager_download_many.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index 55611ca29..f1a1f5c6e 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -12,15 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Example usage: +# python samples/snippets/storage_transfer_manager_download_many.py \ +# --bucket_name \ +# --blobs \ +# --destination_directory \ +# --blob_name_prefix + # [START storage_transfer_manager_download_many] def download_many_blobs_with_transfer_manager( - bucket_name, blob_names, destination_directory="", workers=8 + bucket_name, blob_names, destination_directory="", blob_name_prefix="", workers=8 ): """Download blobs in a list by name, concurrently in a process pool. The filename of each blob once downloaded is derived from the blob name and the `destination_directory `parameter. For complete control of the filename - of each blob, use transfer_manager.download_many() instead. + of each blob, use transfer_manager.download_`many() instead. Directories will be created automatically as needed to accommodate blob names that include slashes. @@ -56,7 +63,11 @@ def download_many_blobs_with_transfer_manager( bucket = storage_client.bucket(bucket_name) results = transfer_manager.download_many_to_path( - bucket, blob_names, destination_directory=destination_directory, max_workers=workers + bucket, + blob_names, + destination_directory=destination_directory, + blob_name_prefix=blob_name_prefix, + max_workers=workers, ) for name, result in zip(blob_names, results): @@ -68,7 +79,7 @@ def download_many_blobs_with_transfer_manager( elif isinstance(result, Warning): print("Skipped download for {} due to warning: {}".format(name, result)) else: - print("Downloaded {} to {}.".format(name, destination_directory + name)) + print("Downloaded {} inside {} directory.".format(name, destination_directory)) # [END storage_transfer_manager_download_many] if __name__ == "__main__": @@ -77,7 +88,7 @@ def download_many_blobs_with_transfer_manager( parser = argparse.ArgumentParser( description="Download blobs in a list by name, concurrently in a process pool." ) - parser.add_argument("--bucket_name", help="The ID of your GCS bucket") + parser.add_argument("--bucket_name", required=True, help="The name of your GCS bucket") parser.add_argument( "--blobs", nargs="+", @@ -89,6 +100,11 @@ def download_many_blobs_with_transfer_manager( default="", help="The directory on your computer to which to download all of the files", ) + parser.add_argument( + "--blob_name_prefix", + default="", + help="A string that will be prepended to each blob_name to determine the source blob name", + ) parser.add_argument( "--workers", type=int, default=8, help="The maximum number of processes to use" ) @@ -99,5 +115,6 @@ def download_many_blobs_with_transfer_manager( bucket_name=args.bucket_name, blob_names=args.blobs, destination_directory=args.destination_directory, + blob_name_prefix=args.blob_name_prefix, workers=args.workers, ) From 40157de9c65bd9f7a610213c838d242f612881b9 Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Tue, 17 Mar 2026 17:19:18 +0000 Subject: [PATCH 07/10] feat(samples): differentiate between Exception and UserWarning in download_many snippet --- samples/snippets/storage_transfer_manager_download_many.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index f1a1f5c6e..4adef765a 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -73,11 +73,10 @@ def download_many_blobs_with_transfer_manager( for name, result in zip(blob_names, results): # The results list is either `None`, an exception, or a warning for each blob in # the input list, in order. - - if isinstance(result, Exception): - print("Failed to download {} due to exception: {}".format(name, result)) - elif isinstance(result, Warning): + if isinstance(result, UserWarning): print("Skipped download for {} due to warning: {}".format(name, result)) + elif isinstance(result, Exception): + print("Failed to download {} due to exception: {}".format(name, result)) else: print("Downloaded {} inside {} directory.".format(name, destination_directory)) # [END storage_transfer_manager_download_many] From 2d25c806df7e9a4770380e0b90c7da4a627fe92e Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 18 Mar 2026 13:45:34 +0000 Subject: [PATCH 08/10] Resolve merge conflict in download_many_to_path --- google/cloud/storage/transfer_manager.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/google/cloud/storage/transfer_manager.py b/google/cloud/storage/transfer_manager.py index d35ed309b..7f4173690 100644 --- a/google/cloud/storage/transfer_manager.py +++ b/google/cloud/storage/transfer_manager.py @@ -858,11 +858,7 @@ def download_many_to_path( raise_exception=raise_exception, worker_type=worker_type, max_workers=max_workers, -<<<<<<< update-download-many-snippet - skip_if_exists=False, -======= skip_if_exists=False, # skip_if_exists is handled in the loop above ->>>>>>> main ) for meta_index, result in zip(indices_to_process, many_results): @@ -870,10 +866,6 @@ def download_many_to_path( return results -<<<<<<< update-download-many-snippet -======= - ->>>>>>> main def download_chunks_concurrently( blob, From ca91c808a0837998d298b365a5ca8151244406fe Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 18 Mar 2026 14:01:18 +0000 Subject: [PATCH 09/10] Format storage_transfer_manager_download_many.py snippet --- .../storage_transfer_manager_download_many.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index 4adef765a..c9ec897a7 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -19,6 +19,7 @@ # --destination_directory \ # --blob_name_prefix + # [START storage_transfer_manager_download_many] def download_many_blobs_with_transfer_manager( bucket_name, blob_names, destination_directory="", blob_name_prefix="", workers=8 @@ -78,7 +79,11 @@ def download_many_blobs_with_transfer_manager( elif isinstance(result, Exception): print("Failed to download {} due to exception: {}".format(name, result)) else: - print("Downloaded {} inside {} directory.".format(name, destination_directory)) + print( + "Downloaded {} inside {} directory.".format(name, destination_directory) + ) + + # [END storage_transfer_manager_download_many] if __name__ == "__main__": @@ -87,7 +92,9 @@ def download_many_blobs_with_transfer_manager( parser = argparse.ArgumentParser( description="Download blobs in a list by name, concurrently in a process pool." ) - parser.add_argument("--bucket_name", required=True, help="The name of your GCS bucket") + parser.add_argument( + "--bucket_name", required=True, help="The name of your GCS bucket" + ) parser.add_argument( "--blobs", nargs="+", From 60c2b7572568be42421803110cb50e47b981fbdb Mon Sep 17 00:00:00 2001 From: Chandra Sirimala Date: Wed, 18 Mar 2026 14:06:00 +0000 Subject: [PATCH 10/10] Fix typo in download_many_blobs_with_transfer_manager docstring --- samples/snippets/storage_transfer_manager_download_many.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/storage_transfer_manager_download_many.py b/samples/snippets/storage_transfer_manager_download_many.py index c9ec897a7..447d0869c 100644 --- a/samples/snippets/storage_transfer_manager_download_many.py +++ b/samples/snippets/storage_transfer_manager_download_many.py @@ -28,7 +28,7 @@ def download_many_blobs_with_transfer_manager( The filename of each blob once downloaded is derived from the blob name and the `destination_directory `parameter. For complete control of the filename - of each blob, use transfer_manager.download_`many() instead. + of each blob, use transfer_manager.download_many() instead. Directories will be created automatically as needed to accommodate blob names that include slashes.