Skip to content

Commit 8a77ee9

Browse files
committed
Wrap download functions in FileDownload class
Encapsulate download fucntionality in a FileDownload class which adds file handling capabilities to fetcher. Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
1 parent f94a2b2 commit 8a77ee9

2 files changed

Lines changed: 80 additions & 77 deletions

File tree

tuf/ngclient/_internal/download.py

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -27,76 +27,82 @@
2727
from urllib import parse
2828

2929
from tuf import exceptions
30+
from tuf.ngclient._internal.requests_fetcher import RequestsFetcher
3031

3132
logger = logging.getLogger(__name__)
3233

3334

34-
def download_file(url, required_length, fetcher):
35-
"""
36-
<Purpose>
37-
Given the url and length of the desired file, this function opens a
38-
connection to 'url' and downloads the file up to 'required_length'.
39-
40-
<Arguments>
41-
url:
42-
A URL string that represents the location of the file.
43-
44-
required_length:
45-
An integer value representing the length of the file or an
46-
upper boundary.
47-
48-
<Side Effects>
49-
A file object is created on disk to store the contents of 'url'.
35+
class FileDownloader:
36+
"""Provides methods to download the contents of a URL to a file object.
5037
51-
<Exceptions>
52-
exceptions.DownloadLengthMismatchError, if there was a
53-
mismatch of observed vs expected lengths while downloading the file.
54-
55-
Any other unforeseen runtime exception.
56-
57-
<Returns>
58-
A file object that points to the contents of 'url'.
38+
Attributes:
39+
fetcher: A FetcherInterface implementation which provides
40+
the network IO library.
5941
"""
60-
# 'url.replace('\\', '/')' is needed for compatibility with Windows-based
61-
# systems, because they might use back-slashes in place of forward-slashes.
62-
# This converts it to the common format. unquote() replaces %xx escapes in
63-
# a url with their single-character equivalent. A back-slash may be
64-
# encoded as %5c in the url, which should also be replaced with a forward
65-
# slash.
66-
url = parse.unquote(url).replace("\\", "/")
67-
logger.debug("Downloading: %s", url)
68-
69-
# This is the temporary file that we will return to contain the contents of
70-
# the downloaded file.
71-
temp_file = tempfile.TemporaryFile() # pylint: disable=consider-using-with
72-
73-
number_of_bytes_received = 0
74-
75-
try:
76-
chunks = fetcher.fetch(url, required_length)
77-
for chunk in chunks:
78-
temp_file.write(chunk)
79-
number_of_bytes_received += len(chunk)
80-
81-
if number_of_bytes_received > required_length:
82-
raise exceptions.DownloadLengthMismatchError(
83-
required_length, number_of_bytes_received
84-
)
85-
86-
except Exception:
87-
# Close 'temp_file'. Any written data is lost.
88-
temp_file.close()
89-
raise
90-
91-
else:
92-
temp_file.seek(0)
93-
return temp_file
94-
95-
96-
def download_bytes(url, required_length, fetcher):
97-
"""Download bytes from given url
98-
99-
Returns the downloaded bytes, otherwise like download_file()
100-
"""
101-
with download_file(url, required_length, fetcher) as dl_file:
102-
return dl_file.read()
42+
43+
def __init__(self, fetcher):
44+
if fetcher is None:
45+
fetcher = RequestsFetcher()
46+
47+
self._fetcher = fetcher
48+
49+
def download_file(self, url, required_length):
50+
"""Opens a connection to 'url' and downloads the content
51+
up to 'required_length'.
52+
53+
Args:
54+
url: a URL string that represents the location of the file.
55+
required_length: an integer value representing the length of
56+
the file or an upper boundary.
57+
58+
Raises:
59+
DownloadLengthMismatchError: a mismatch of observed vs expected
60+
lengths while downloading the file.
61+
62+
Returns:
63+
A file object that points to the contents of 'url'.
64+
"""
65+
# 'url.replace('\\', '/')' is needed for compatibility with
66+
# Windows-based systems, because they might use back-slashes in place
67+
# of forward-slashes. This converts it to the common format.
68+
# unquote() replaces %xx escapes in a url with their single-character
69+
# equivalent. A back-slash may beencoded as %5c in the url, which
70+
# should also be replaced with a forward slash.
71+
url = parse.unquote(url).replace("\\", "/")
72+
logger.debug("Downloading: %s", url)
73+
74+
# This is the temporary file that we will return to contain the
75+
# contents of the downloaded file.
76+
temp_file = (
77+
tempfile.TemporaryFile()
78+
) # pylint: disable=consider-using-with
79+
80+
number_of_bytes_received = 0
81+
82+
try:
83+
chunks = self._fetcher.fetch(url, required_length)
84+
for chunk in chunks:
85+
temp_file.write(chunk)
86+
number_of_bytes_received += len(chunk)
87+
88+
if number_of_bytes_received > required_length:
89+
raise exceptions.DownloadLengthMismatchError(
90+
required_length, number_of_bytes_received
91+
)
92+
93+
except Exception:
94+
# Close 'temp_file'. Any written data is lost.
95+
temp_file.close()
96+
raise
97+
98+
else:
99+
temp_file.seek(0)
100+
return temp_file
101+
102+
def download_bytes(self, url, required_length):
103+
"""Download bytes from given url
104+
105+
Returns the downloaded bytes, otherwise like download_file()
106+
"""
107+
with self.download_file(url, required_length) as dl_file:
108+
return dl_file.read()

tuf/ngclient/updater.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from securesystemslib import util as sslib_util
1616

1717
from tuf import exceptions
18-
from tuf.ngclient._internal import download, metadata_bundle, requests_fetcher
19-
from tuf.ngclient.fetcher import FetcherInterface
18+
from tuf.ngclient._internal import metadata_bundle
19+
from tuf.ngclient._internal.download import FileDownloader
2020

2121
# Globals
2222
MAX_ROOT_ROTATIONS = 32
@@ -40,7 +40,7 @@ def __init__(
4040
repository_dir: str,
4141
metadata_base_url: str,
4242
target_base_url: Optional[str] = None,
43-
fetcher: Optional[FetcherInterface] = None,
43+
fetcher: Optional["FetcherInterface"] = None,
4444
):
4545
"""
4646
Args:
@@ -67,10 +67,7 @@ def __init__(
6767
data = self._load_local_metadata("root")
6868
self._bundle = metadata_bundle.MetadataBundle(data)
6969

70-
if fetcher is None:
71-
self._fetcher = requests_fetcher.RequestsFetcher()
72-
else:
73-
self._fetcher = fetcher
70+
self.downloader = FileDownloader(fetcher)
7471

7572
def refresh(self) -> None:
7673
"""
@@ -200,8 +197,8 @@ def download_target(
200197

201198
full_url = parse.urljoin(target_base_url, targetinfo["filepath"])
202199

203-
with download.download_file(
204-
full_url, targetinfo["fileinfo"].length, self._fetcher
200+
with self.downloader.download_file(
201+
full_url, targetinfo["fileinfo"].length
205202
) as target_file:
206203
_check_file_length(target_file, targetinfo["fileinfo"].length)
207204
_check_hashes_obj(target_file, targetinfo["fileinfo"].hashes)
@@ -220,7 +217,7 @@ def _download_metadata(
220217
else:
221218
filename = f"{version}.{rolename}.json"
222219
url = parse.urljoin(self._metadata_base_url, filename)
223-
return download.download_bytes(url, length, self._fetcher)
220+
return self.downloader.download_bytes(url, length)
224221

225222
def _load_local_metadata(self, rolename: str) -> bytes:
226223
with open(os.path.join(self._dir, f"{rolename}.json"), "rb") as f:

0 commit comments

Comments
 (0)