diff --git a/dvc/fs/__init__.py b/dvc/fs/__init__.py index b37731c97..f5d36f314 100644 --- a/dvc/fs/__init__.py +++ b/dvc/fs/__init__.py @@ -1,3 +1,4 @@ +import glob from typing import Optional from urllib.parse import urlparse @@ -70,7 +71,12 @@ def download( if isinstance(fs, DVCFileSystem): lfs_prefetch( - fs, [f"{fs.normpath(fs_path)}/**" if fs.isdir(fs_path) else fs_path] + fs, + [ + f"{fs.normpath(glob.escape(fs_path))}/**" + if fs.isdir(fs_path) + else glob.escape(fs_path) + ], ) cb.set_size(len(from_infos)) jobs = jobs or fs.jobs diff --git a/tests/func/test_download.py b/tests/func/test_download.py new file mode 100644 index 000000000..343dbef65 --- /dev/null +++ b/tests/func/test_download.py @@ -0,0 +1,73 @@ +import os +from unittest.mock import ANY + +import pytest + +from dvc.fs import download + +lfs_prefetch_params = [ + pytest.param("abc", "abc", id="plain"), + pytest.param( + "*", + "[*]", + marks=pytest.mark.skipif( + os.name == "nt", + reason="forbidden character `*` on Windows filesystem", + ), + id="escape-*", + ), + pytest.param( + "**", + "[*][*]", + marks=pytest.mark.skipif( + os.name == "nt", reason="forbidden character `*` on Windows filesystem" + ), + id="escape-**", + ), + pytest.param( + "?", + "[?]", + marks=pytest.mark.skipif( + os.name == "nt", reason="forbidden character `?` on Windows filesystem" + ), + id="escape-?", + ), + pytest.param("[abc]", "[[]abc]", id="escape-[seq]"), + pytest.param("[!abc]", "[[]!abc]", id="escape-[!seq]"), +] + + +@pytest.mark.parametrize("dirname, include_name", lfs_prefetch_params) +def test_lfs_prefetch_directory(tmp_dir, dvc, scm, mocker, dirname, include_name): + mock_fetch = mocker.patch("scmrepo.git.lfs.fetch") + tmp_dir.scm_gen( + { + ".gitattributes": "data/**/* filter=lfs diff=lfs merge=lfs -text", + f"data/{dirname}/test.txt": "test data", + }, + commit="init lfs", + ) + rev = scm.get_rev() + with dvc.switch(rev): + download(dvc.dvcfs, f"data/{dirname}", "data") + mock_fetch.assert_called_once_with( + scm, [rev], include=[f"/data/{include_name}/**"], progress=ANY + ) + + +@pytest.mark.parametrize("basename, include_name", lfs_prefetch_params) +def test_lfs_prefetch_file(tmp_dir, dvc, scm, mocker, basename, include_name): + mock_fetch = mocker.patch("scmrepo.git.lfs.fetch") + tmp_dir.scm_gen( + { + ".gitattributes": "data/**/* filter=lfs diff=lfs merge=lfs -text", + f"data/{basename}.txt": "test data", + }, + commit="init lfs", + ) + rev = scm.get_rev() + with dvc.switch(rev): + download(dvc.dvcfs, f"data/{basename}.txt", "data") + mock_fetch.assert_called_once_with( + scm, [rev], include=[f"/data/{include_name}.txt"], progress=ANY + )