Skip to content
47 changes: 41 additions & 6 deletions monai/apps/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from monai.config.type_definitions import PathLike
from monai.utils import look_up_option, min_version, optional_import
from monai.utils.deprecate_utils import warn_deprecated

requests, has_requests = optional_import("requests")
gdown, has_gdown = optional_import("gdown", "4.7.3")
Expand Down Expand Up @@ -177,6 +178,9 @@ def check_hash(filepath: PathLike, val: str | None = None, hash_type: str = "sha
The supported hash types are `"md5"`, `"sha1"`, `"sha256"`, `"sha512"`.
See also: :py:data:`monai.apps.utils.SUPPORTED_HASH_TYPES`.

.. versionchanged:: 1.7
The default `hash_type` changed from "md5" to "sha256" for stronger integrity verification.
Pass `hash_type="md5"` explicitly to verify against MD5 hashes.
"""
if val is None:
warnings.warn(f"No hash value provided for {filepath}; file integrity is NOT verified.", stacklevel=2)
Expand Down Expand Up @@ -204,7 +208,7 @@ def download_url(
url: str,
filepath: PathLike = "",
hash_val: str | None = None,
hash_type: str = "sha256",
hash_type: str | None = None,
progress: bool = True,
**gdown_kwargs: Any,
) -> None:
Expand All @@ -217,7 +221,7 @@ def download_url(
If undefined, `os.path.basename(url)` will be used.
hash_val: expected hash value to validate the downloaded file.
if None, skip hash validation.
hash_type: type of hash algorithm to use, default is `"sha256"`.
hash_type: type of hash algorithm to use, default is None which will select `"sha256"`.
The supported hash types are `"md5"`, `"sha1"`, `"sha256"`, `"sha512"`.
progress: whether to display a progress bar.
gdown_kwargs: other args for `gdown` except for the `url`, `output` and `quiet`.
Expand All @@ -234,7 +238,23 @@ def download_url(
ContentTooShortError: See urllib.request.urlretrieve.
IOError: See urllib.request.urlretrieve.
HashCheckError: When the hash validation of the ``url`` downloaded file fails.

.. versionchanged:: 1.7
The default `hash_type` changed from "md5" to None for stronger integrity verification. If this is left None
when a `hash_val` value is provided, this will warn to explicitly set `hash_type` then choose sha256 hashing.
Pass `hash_type="md5"` explicitly to verify against MD5 hashes and suppress the warning. In MONAI 1.8 the
default will be set to "sha256".
"""
if hash_val is not None and hash_type is None:
warn_deprecated(
"monai.apps.utils.download_url",
'Default `hash_type` value changed to `None` from "md5" in MONAI 1.7, '
"set to explicit value to suppress this warning. Defaulting to sha256 checking. In MONAI 1.8 the "
'default will be set to "sha256".',
stacklevel=3,
)
hash_type = "sha256"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if not filepath:
filepath = Path(".", _basename(url)).resolve()
logger.info(f"Default downloading to '{filepath}'")
Expand Down Expand Up @@ -316,7 +336,7 @@ def extractall(
filepath: PathLike,
output_dir: PathLike = ".",
hash_val: str | None = None,
hash_type: str = "sha256",
hash_type: str | None = None,
file_type: str = "",
has_base: bool = True,
) -> None:
Expand All @@ -329,7 +349,7 @@ def extractall(
output_dir: target directory to save extracted files.
hash_val: expected hash value to validate the compressed file.
if None, skip hash validation.
hash_type: type of hash algorithm to use, default is `"sha256"`.
hash_type: type of hash algorithm to use, default is None which will select `"sha256"`.
file_type: string of file type for decompressing. Leave it empty to infer the type from the filepath basename.
has_base: whether the extracted files have a base folder. This flag is used when checking if the existing
folder is a result of `extractall`, if it is, the extraction is skipped. For example, if A.zip is unzipped
Expand All @@ -340,7 +360,22 @@ def extractall(
HashCheckError: When the hash validation of the ``filepath`` compressed file fails.
NotImplementedError: When the ``filepath`` file extension is not one of [zip", "tar.gz", "tar"].

.. versionchanged:: 1.6.1
The default `hash_type` changed from "md5" to None for stronger integrity verification. If this is left None
when a `hash_val` value is provided, this will warn to explicitly set `hash_type` then choose sha256 hashing.
Pass `hash_type="md5"` explicitly to verify against MD5 hashes and suppress the warning. In MONAI 1.8 the
default will be set to "sha256".
"""
if hash_val is not None and hash_type is None:
warn_deprecated(
"monai.apps.utils.extractall",
'Default `hash_type` value changed to `None` from "md5" in MONAI 1.7, '
"set to an explicit value to suppress this warning. Defaulting to sha256 checking. In MONAI 1.8 the "
'default will be set to "sha256".',
stacklevel=3,
)
hash_type = "sha256"

filepath = Path(filepath)
if hash_val and not check_hash(filepath, hash_val, hash_type):
raise HashCheckError(
Expand Down Expand Up @@ -395,7 +430,7 @@ def download_and_extract(
filepath: PathLike = "",
output_dir: PathLike = ".",
hash_val: str | None = None,
hash_type: str = "sha256",
hash_type: str | None = None,
file_type: str = "",
has_base: bool = True,
progress: bool = True,
Expand All @@ -411,7 +446,7 @@ def download_and_extract(
default is the current directory.
hash_val: expected hash value to validate the downloaded file.
if None, skip hash validation.
hash_type: type of hash algorithm to use, default is `"sha256"`.
hash_type: type of hash algorithm to use, default is None which will select `"sha256"`.
file_type: string of file type for decompressing. Leave it empty to infer the type from url's base file name.
has_base: whether the extracted files have a base folder. This flag is used when checking if the existing
folder is a result of `extractall`, if it is, the extraction is skipped. For example, if A.zip is unzipped
Expand Down
4 changes: 2 additions & 2 deletions monai/utils/deprecate_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ class DeprecatedError(Exception):
pass


def warn_deprecated(obj, msg, warning_category=FutureWarning):
def warn_deprecated(obj, msg, warning_category=FutureWarning, stacklevel=2):
"""
Issue the warning message `msg`.
"""
warnings.warn(f"{obj}: {msg}", category=warning_category, stacklevel=2)
warnings.warn(f"{obj}: {msg}", category=warning_category, stacklevel=stacklevel)


def deprecated(
Expand Down
8 changes: 8 additions & 0 deletions tests/apps/test_download_and_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def test_download_url_hash_mismatch(self):
with self.assertRaises(HashCheckError):
download_url(self.url, filepath, hash_val="0" * len(self.hash_val), hash_type=self.hash_type)

# test the warning is raised if no hash_type is given
with self.assertWarns(FutureWarning):
download_url(self.url, filepath, hash_val=self.hash_val)

@skip_if_quick
def test_extractall_hash_mismatch(self):
"""extractall should raise HashCheckError when hash is incorrect."""
Expand All @@ -70,6 +74,10 @@ def test_extractall_hash_mismatch(self):
with self.assertRaises(HashCheckError):
extractall(filepath, output_dir, hash_val="0" * len(self.hash_val), hash_type=self.hash_type)

# test the warning is raised if no hash_type is given
with self.assertWarns(FutureWarning):
extractall(filepath, output_dir, hash_val=self.hash_val)

@skip_if_quick
@parameterized.expand([("icon", "tar"), ("favicon", "zip")])
def test_download_and_extract_various_formats(self, key, file_type):
Expand Down
2 changes: 1 addition & 1 deletion tests/handlers/test_handler_mlflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ def test_dataset_tracking(self):
data_dir = os.path.join(tempdir, "endoscopic_tool_dataset")
with skip_if_downloading_fails():
if not os.path.exists(data_dir):
download_and_extract(resource, compressed_file, tempdir, md5)
download_and_extract(resource, compressed_file, tempdir, md5, "md5")

download(test_bundle_name, bundle_dir=tempdir)

Expand Down
Loading