fosslight-util 2.2.3__tar.gz → 2.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fosslight_util-2.2.3/src/fosslight_util.egg-info → fosslight_util-2.2.4}/PKG-INFO +5 -1
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/README.md +4 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/pyproject.toml +1 -1
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/download.py +660 -144
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/help.py +4 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4/src/fosslight_util.egg-info}/PKG-INFO +5 -1
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/LICENSE +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/MANIFEST.in +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/setup.cfg +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/__init__.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/_get_downloadable_url.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/compare_yaml.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/constant.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/correct.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/cover.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/exclude.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/get_pom_license.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/oss_item.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/output_format.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/parsing_yaml.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/read_excel.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/frequentLicenselist.json +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/frequent_license_nick_list.json +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/licenses.json +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/set_log.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/spdx_licenses.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/time.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/timer_thread.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_cyclonedx.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_excel.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_opossum.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_scancodejson.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_spdx.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_txt.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_yaml.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/SOURCES.txt +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/dependency_links.txt +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/entry_points.txt +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/requires.txt +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/top_level.txt +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_cyclonedx.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_decide_checkout.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download_packagist.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download_version_hint.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_opossum.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_spdx_licenses.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_text.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_write_excel_hidden_sheet.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_write_output.py +0 -0
- {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_write_yaml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fosslight_util
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: FOSSLight Util
|
|
5
5
|
Author: LG Electronics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -165,6 +165,10 @@ If you give a link, the source is downloaded to the target directory through git
|
|
|
165
165
|
```
|
|
166
166
|
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/
|
|
167
167
|
```
|
|
168
|
+
Limit download size (GB) with `--size-limit` / `-l` (omit for unlimited):
|
|
169
|
+
```
|
|
170
|
+
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/ -l 5
|
|
171
|
+
```
|
|
168
172
|
If you want to try with private repository, set your github token like below.
|
|
169
173
|
```
|
|
170
174
|
$ fosslight_download -s "https://my_github_token@github.com/Foo/private_repo -t target_dir/"
|
|
@@ -130,6 +130,10 @@ If you give a link, the source is downloaded to the target directory through git
|
|
|
130
130
|
```
|
|
131
131
|
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/
|
|
132
132
|
```
|
|
133
|
+
Limit download size (GB) with `--size-limit` / `-l` (omit for unlimited):
|
|
134
|
+
```
|
|
135
|
+
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/ -l 5
|
|
136
|
+
```
|
|
133
137
|
If you want to try with private repository, set your github token like below.
|
|
134
138
|
```
|
|
135
139
|
$ fosslight_download -s "https://my_github_token@github.com/Foo/private_repo -t target_dir/"
|
|
@@ -45,6 +45,10 @@ compression_extension = {
|
|
|
45
45
|
}
|
|
46
46
|
prefix_refs = ["refs/remotes/origin/", "refs/tags/"]
|
|
47
47
|
SIGNAL_TIMEOUT = 600
|
|
48
|
+
SIZE_CHECK_INTERVAL_SECONDS = 10
|
|
49
|
+
_BYTES_PER_GB = 1024 ** 3
|
|
50
|
+
# Active Windows Alarm so nested helpers (e.g. git clone) can cancel it.
|
|
51
|
+
_active_download_alarm = None
|
|
48
52
|
|
|
49
53
|
# Some mirrors (e.g. mirrors.ustc.edu.cn) return 403 for python-requests' default
|
|
50
54
|
# User-Agent, or 200 with a small text/html interstitial. Start with a curl-style UA
|
|
@@ -55,11 +59,22 @@ _HTTP_BROWSER_USER_AGENT = (
|
|
|
55
59
|
)
|
|
56
60
|
_HTTP_CURL_LIKE_USER_AGENT = "curl/8.7.1"
|
|
57
61
|
|
|
62
|
+
# Substrings from actual size-limit error messages only
|
|
63
|
+
# (git clone size guard / HTTP-wget size checks).
|
|
64
|
+
_SIZE_LIMIT_BLOCK_MARKERS = (
|
|
65
|
+
"temporary directory size exceeded", # Download aborted: temporary directory size exceeded limit ...
|
|
66
|
+
"Download aborted due to size limit",
|
|
67
|
+
)
|
|
68
|
+
|
|
58
69
|
|
|
59
70
|
class _HtmlWhenBinaryExpected(Exception):
|
|
60
71
|
"""Server returned HTML for a URL that should be a binary archive; try another client."""
|
|
61
72
|
|
|
62
73
|
|
|
74
|
+
class SizeLimitExceeded(Exception):
|
|
75
|
+
"""Raised when an HTTP/wget download exceeds ``size_limit_gb``."""
|
|
76
|
+
|
|
77
|
+
|
|
63
78
|
def _url_looks_like_binary_archive(url: str) -> bool:
|
|
64
79
|
path = urllib.parse.urlparse(url).path.lower()
|
|
65
80
|
return any(path.endswith(ext) for ext in compression_extension)
|
|
@@ -74,16 +89,54 @@ def _download_http_header_attempts():
|
|
|
74
89
|
|
|
75
90
|
|
|
76
91
|
class Alarm(threading.Thread):
|
|
92
|
+
"""Windows download watchdog; call ``cancel()`` to stop before timeout."""
|
|
93
|
+
|
|
77
94
|
def __init__(self, timeout):
|
|
78
95
|
threading.Thread.__init__(self)
|
|
79
96
|
self.timeout = timeout
|
|
80
|
-
self.
|
|
97
|
+
self._cancelled = threading.Event()
|
|
98
|
+
self.daemon = True
|
|
81
99
|
|
|
82
100
|
def run(self):
|
|
83
|
-
time.sleep
|
|
101
|
+
# Wait until timeout or cancel(); do not use bare time.sleep.
|
|
102
|
+
if self._cancelled.wait(self.timeout):
|
|
103
|
+
return
|
|
84
104
|
logger.error("download timeout! (%d sec)", SIGNAL_TIMEOUT)
|
|
85
105
|
os._exit(1)
|
|
86
106
|
|
|
107
|
+
def cancel(self):
|
|
108
|
+
"""Stop the watchdog so a successful download is not killed later."""
|
|
109
|
+
self._cancelled.set()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _start_download_watchdog():
|
|
113
|
+
"""Start SIGALRM (Unix) or Alarm thread (Windows). Return Alarm or None."""
|
|
114
|
+
global _active_download_alarm
|
|
115
|
+
if platform.system() != "Windows":
|
|
116
|
+
signal.signal(signal.SIGALRM, alarm_handler)
|
|
117
|
+
signal.alarm(SIGNAL_TIMEOUT)
|
|
118
|
+
return None
|
|
119
|
+
alarm = Alarm(SIGNAL_TIMEOUT)
|
|
120
|
+
alarm.start()
|
|
121
|
+
_active_download_alarm = alarm
|
|
122
|
+
return alarm
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cancel_download_watchdog(alarm=None):
|
|
126
|
+
"""Cancel SIGALRM or the Windows Alarm watchdog."""
|
|
127
|
+
global _active_download_alarm
|
|
128
|
+
if platform.system() != "Windows":
|
|
129
|
+
try:
|
|
130
|
+
signal.alarm(0)
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return
|
|
134
|
+
target = alarm if alarm is not None else _active_download_alarm
|
|
135
|
+
if target is not None:
|
|
136
|
+
target.cancel()
|
|
137
|
+
if alarm is None or alarm is _active_download_alarm:
|
|
138
|
+
_active_download_alarm = None
|
|
139
|
+
|
|
87
140
|
|
|
88
141
|
class TimeOutException(Exception):
|
|
89
142
|
def __init__(self, message, error_code):
|
|
@@ -141,6 +194,53 @@ def change_ssh_link_to_https(src_link):
|
|
|
141
194
|
return src_link
|
|
142
195
|
|
|
143
196
|
|
|
197
|
+
def _is_size_limit_block_message(msg: str) -> bool:
|
|
198
|
+
if not msg:
|
|
199
|
+
return False
|
|
200
|
+
return any(marker in msg for marker in _SIZE_LIMIT_BLOCK_MARKERS)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _reject_empty_or_html_download(path: str, link: str) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Reject empty or HTML/XML error-page downloads after HTTP fetch.
|
|
206
|
+
|
|
207
|
+
Matches former scancode fetch_http post-wget checks so analysis does not
|
|
208
|
+
treat HTML interstitial/error pages as valid input.
|
|
209
|
+
"""
|
|
210
|
+
if not path or not os.path.isfile(path):
|
|
211
|
+
raise Exception(f"Download failed: {link}")
|
|
212
|
+
try:
|
|
213
|
+
size = os.path.getsize(path)
|
|
214
|
+
except OSError:
|
|
215
|
+
raise Exception(f"Download failed: {link}")
|
|
216
|
+
if size == 0:
|
|
217
|
+
try:
|
|
218
|
+
os.remove(path)
|
|
219
|
+
except OSError:
|
|
220
|
+
pass
|
|
221
|
+
raise Exception(f"wget downloaded an empty file: {link}")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
with open(path, "rb") as f:
|
|
225
|
+
raw = f.read(512)
|
|
226
|
+
except OSError as error:
|
|
227
|
+
raise Exception(f"Download failed: {link}") from error
|
|
228
|
+
|
|
229
|
+
content_start = raw.lstrip(b"\xef\xbb\xbf \t\r\n").lower()
|
|
230
|
+
html_signatures = (b"<!doctype", b"<html", b"<head", b"<body", b"<?xml")
|
|
231
|
+
suffix = Path(path).suffix.lower()
|
|
232
|
+
if suffix in (".html", ".htm") or any(
|
|
233
|
+
content_start.startswith(sig) for sig in html_signatures
|
|
234
|
+
):
|
|
235
|
+
try:
|
|
236
|
+
os.remove(path)
|
|
237
|
+
except OSError:
|
|
238
|
+
pass
|
|
239
|
+
raise Exception(
|
|
240
|
+
f"wget downloaded an HTML page instead of the expected file: {link}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
144
244
|
def parse_src_link(src_link):
|
|
145
245
|
src_info = {"url": src_link}
|
|
146
246
|
src_link_changed = ""
|
|
@@ -169,7 +269,9 @@ def cli_download_and_extract(link: str, target_dir: str, log_dir: str, checkout_
|
|
|
169
269
|
compressed_only: bool = False, ssh_key: str = "",
|
|
170
270
|
id: str = "", git_token: str = "",
|
|
171
271
|
called_cli: bool = True,
|
|
172
|
-
output: bool = False
|
|
272
|
+
output: bool = False,
|
|
273
|
+
size_limit_gb: Optional[float] = None
|
|
274
|
+
) -> Tuple[bool, str, str, str, str]:
|
|
173
275
|
global logger
|
|
174
276
|
|
|
175
277
|
success = True
|
|
@@ -178,6 +280,7 @@ def cli_download_and_extract(link: str, target_dir: str, log_dir: str, checkout_
|
|
|
178
280
|
oss_name = ""
|
|
179
281
|
oss_version = ""
|
|
180
282
|
downloaded_link = ""
|
|
283
|
+
size_limit_blocked = False
|
|
181
284
|
log_file_name = "fosslight_download_" + \
|
|
182
285
|
datetime.now().strftime('%Y%m%d_%H-%M-%S')+".txt"
|
|
183
286
|
logger, log_item = init_log(os.path.join(log_dir, log_file_name))
|
|
@@ -201,30 +304,54 @@ def cli_download_and_extract(link: str, target_dir: str, log_dir: str, checkout_
|
|
|
201
304
|
branch = ''.join(src_info.get("branch", "")).split('=')[-1]
|
|
202
305
|
is_rubygems = src_info.get("rubygems", False)
|
|
203
306
|
|
|
204
|
-
# General download
|
|
307
|
+
# General download: git clone → requests → system wget
|
|
205
308
|
success_git, msg, oss_name, oss_version, _ = download_git_clone(
|
|
206
309
|
link, target_dir, checkout_to, tag, branch,
|
|
207
|
-
ssh_key, id, git_token, called_cli)
|
|
310
|
+
ssh_key, id, git_token, called_cli, size_limit_gb=size_limit_gb)
|
|
208
311
|
link = change_ssh_link_to_https(link)
|
|
209
312
|
if success_git:
|
|
210
313
|
downloaded_link = link
|
|
211
|
-
|
|
314
|
+
success = True
|
|
315
|
+
size_limit_blocked = _is_size_limit_block_message(msg)
|
|
316
|
+
if size_limit_blocked:
|
|
317
|
+
success = False
|
|
318
|
+
elif (not is_rubygems) and (not success_git):
|
|
212
319
|
if os.path.isfile(target_dir):
|
|
213
320
|
shutil.rmtree(target_dir)
|
|
214
321
|
|
|
215
322
|
success, downloaded_file, msg_wget, oss_name, oss_version, resolved_link = download_wget(
|
|
216
|
-
link, target_dir, compressed_only, checkout_to
|
|
323
|
+
link, target_dir, compressed_only, checkout_to,
|
|
324
|
+
size_limit_gb=size_limit_gb,
|
|
217
325
|
)
|
|
218
|
-
if
|
|
219
|
-
|
|
326
|
+
if _is_size_limit_block_message(msg_wget):
|
|
327
|
+
size_limit_blocked = True
|
|
328
|
+
success = False
|
|
329
|
+
msg = msg_wget
|
|
330
|
+
elif success and downloaded_file:
|
|
331
|
+
success = extract_compressed_file(
|
|
332
|
+
downloaded_file, target_dir, True, compressed_only
|
|
333
|
+
)
|
|
220
334
|
if success:
|
|
221
335
|
downloaded_link = resolved_link
|
|
336
|
+
else:
|
|
337
|
+
# Extract failed (e.g. non-archive when compressed_only): fail
|
|
338
|
+
msg_wget = (
|
|
339
|
+
f"Failed to extract downloaded file: {downloaded_file}"
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
if os.path.isfile(downloaded_file):
|
|
343
|
+
os.remove(downloaded_file)
|
|
344
|
+
except OSError:
|
|
345
|
+
pass
|
|
222
346
|
# Download from rubygems.org
|
|
223
347
|
elif is_rubygems and shutil.which("gem"):
|
|
224
348
|
success = gem_download(link, target_dir, checkout_to)
|
|
225
349
|
if success:
|
|
226
350
|
downloaded_link = link
|
|
227
|
-
if
|
|
351
|
+
if size_limit_blocked:
|
|
352
|
+
# Keep a clear size-limit message for callers/UI (do not wrap as git/wget fail)
|
|
353
|
+
pass
|
|
354
|
+
elif msg:
|
|
228
355
|
msg = f'git fail: {msg}'
|
|
229
356
|
if is_rubygems:
|
|
230
357
|
msg = f'gem download: {success}'
|
|
@@ -680,11 +807,207 @@ def get_github_token(git_url):
|
|
|
680
807
|
return github_token
|
|
681
808
|
|
|
682
809
|
|
|
683
|
-
def
|
|
810
|
+
def _size_limit_bytes(size_limit_gb: Optional[float]) -> Optional[int]:
|
|
811
|
+
if size_limit_gb is None or size_limit_gb <= 0:
|
|
812
|
+
return None
|
|
813
|
+
return int(float(size_limit_gb) * _BYTES_PER_GB)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _size_limit_abort_message(
|
|
817
|
+
size_limit_gb: float, when: str, observed_bytes: int = 0
|
|
818
|
+
) -> str:
|
|
819
|
+
observed = max(0, int(observed_bytes or 0))
|
|
820
|
+
observed_gb = observed / _BYTES_PER_GB
|
|
821
|
+
return (
|
|
822
|
+
f"Download aborted: temporary directory size exceeded "
|
|
823
|
+
f"limit ({float(size_limit_gb):g} GB) {when} "
|
|
824
|
+
f"(observed {observed_gb:.2f}GB, observed_bytes={observed})."
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _content_length_from_headers(headers) -> int:
|
|
829
|
+
try:
|
|
830
|
+
return int(headers.get("Content-Length") or 0)
|
|
831
|
+
except (TypeError, ValueError):
|
|
832
|
+
return 0
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _raise_if_content_length_over_limit(headers, size_limit_gb: Optional[float], when: str) -> None:
|
|
836
|
+
limit = _size_limit_bytes(size_limit_gb)
|
|
837
|
+
if limit is None:
|
|
838
|
+
return
|
|
839
|
+
content_length = _content_length_from_headers(headers)
|
|
840
|
+
if content_length >= limit:
|
|
841
|
+
raise SizeLimitExceeded(
|
|
842
|
+
_size_limit_abort_message(size_limit_gb, when, content_length)
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _raise_if_bytes_over_limit(num_bytes: int, size_limit_gb: Optional[float], when: str) -> None:
|
|
847
|
+
limit = _size_limit_bytes(size_limit_gb)
|
|
848
|
+
if limit is None:
|
|
849
|
+
return
|
|
850
|
+
if num_bytes >= limit:
|
|
851
|
+
raise SizeLimitExceeded(
|
|
852
|
+
_size_limit_abort_message(size_limit_gb, when, num_bytes)
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _dir_size_bytes(path: str) -> int:
|
|
857
|
+
total = 0
|
|
858
|
+
if not path or not os.path.exists(path):
|
|
859
|
+
return 0
|
|
860
|
+
for root, _dirs, files in os.walk(path):
|
|
861
|
+
for name in files:
|
|
862
|
+
fp = os.path.join(root, name)
|
|
863
|
+
try:
|
|
864
|
+
if os.path.islink(fp):
|
|
865
|
+
continue
|
|
866
|
+
total += os.path.getsize(fp)
|
|
867
|
+
except OSError:
|
|
868
|
+
continue
|
|
869
|
+
return total
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def build_shallow_clone_cmd(git_url: str, target_dir: str, refs_to_checkout: str = "") -> List[str]:
|
|
873
|
+
cmd = [
|
|
874
|
+
"git", "-c", "credential.helper=", "-c", "credential.helper=!",
|
|
875
|
+
"clone", "--depth", "1", "--single-branch",
|
|
876
|
+
]
|
|
877
|
+
if refs_to_checkout:
|
|
878
|
+
cmd.extend(["--branch", refs_to_checkout])
|
|
879
|
+
cmd.extend([git_url, target_dir])
|
|
880
|
+
return cmd
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _cleanup_target_dir(target_dir: str) -> None:
|
|
884
|
+
try:
|
|
885
|
+
if target_dir and os.path.exists(target_dir):
|
|
886
|
+
shutil.rmtree(target_dir, ignore_errors=True)
|
|
887
|
+
except Exception as e:
|
|
888
|
+
logger.info(f"Failed to remove target dir {target_dir}: {e}")
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def run_git_clone_with_size_guard(
|
|
892
|
+
cmd: List[str],
|
|
893
|
+
env: dict,
|
|
894
|
+
target_dir: str,
|
|
895
|
+
size_limit_gb: Optional[float] = None,
|
|
896
|
+
size_check_after_sec: int = SIGNAL_TIMEOUT,
|
|
897
|
+
size_check_interval_sec: int = SIZE_CHECK_INTERVAL_SECONDS,
|
|
898
|
+
) -> Tuple[bool, str]:
|
|
899
|
+
"""Run git clone via Popen with delayed, periodic, and post-clone size checks.
|
|
900
|
+
|
|
901
|
+
After ``size_check_after_sec``, check directory size once while still running.
|
|
902
|
+
If under the limit, re-check every ``size_check_interval_sec``.
|
|
903
|
+
When clone finishes successfully, check the final directory size again.
|
|
904
|
+
Over limit → kill (if needed), cleanup, fail.
|
|
905
|
+
|
|
906
|
+
Returns (success, error_message).
|
|
907
|
+
"""
|
|
908
|
+
limit = _size_limit_bytes(size_limit_gb)
|
|
909
|
+
try:
|
|
910
|
+
proc = subprocess.Popen(
|
|
911
|
+
cmd,
|
|
912
|
+
env=env,
|
|
913
|
+
stdout=subprocess.PIPE,
|
|
914
|
+
stderr=subprocess.PIPE,
|
|
915
|
+
text=True,
|
|
916
|
+
stdin=subprocess.DEVNULL,
|
|
917
|
+
)
|
|
918
|
+
except Exception as e:
|
|
919
|
+
return False, str(e)
|
|
920
|
+
|
|
921
|
+
def _fail_for_size(elapsed_hint: str, current: int, kill_proc: bool = True) -> Tuple[bool, str]:
|
|
922
|
+
logger.info(
|
|
923
|
+
f"Clone exceeded size limit after {elapsed_hint} "
|
|
924
|
+
f"({current} bytes >= {limit}); aborting."
|
|
925
|
+
)
|
|
926
|
+
if kill_proc:
|
|
927
|
+
try:
|
|
928
|
+
proc.kill()
|
|
929
|
+
except Exception:
|
|
930
|
+
pass
|
|
931
|
+
try:
|
|
932
|
+
proc.communicate(timeout=60)
|
|
933
|
+
except Exception:
|
|
934
|
+
pass
|
|
935
|
+
_cleanup_target_dir(target_dir)
|
|
936
|
+
return False, _size_limit_abort_message(
|
|
937
|
+
size_limit_gb, f"after {elapsed_hint}", current
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
stdout = stderr = ""
|
|
941
|
+
try:
|
|
942
|
+
next_timeout = size_check_after_sec
|
|
943
|
+
first_check_done = False
|
|
944
|
+
while True:
|
|
945
|
+
try:
|
|
946
|
+
stdout, stderr = proc.communicate(timeout=next_timeout)
|
|
947
|
+
break
|
|
948
|
+
except subprocess.TimeoutExpired:
|
|
949
|
+
if limit is not None:
|
|
950
|
+
current = _dir_size_bytes(target_dir)
|
|
951
|
+
if current >= limit:
|
|
952
|
+
elapsed = (
|
|
953
|
+
f"{size_check_after_sec} seconds"
|
|
954
|
+
if not first_check_done
|
|
955
|
+
else (
|
|
956
|
+
f"{size_check_after_sec}s + periodic "
|
|
957
|
+
f"{size_check_interval_sec}s checks"
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
return _fail_for_size(elapsed, current, kill_proc=True)
|
|
961
|
+
first_check_done = True
|
|
962
|
+
# Under limit (or no limit): keep waiting in interval slices
|
|
963
|
+
next_timeout = size_check_interval_sec
|
|
964
|
+
except Exception as e:
|
|
965
|
+
try:
|
|
966
|
+
proc.kill()
|
|
967
|
+
except Exception:
|
|
968
|
+
pass
|
|
969
|
+
return False, str(e)
|
|
970
|
+
|
|
971
|
+
if proc.returncode != 0:
|
|
972
|
+
err = (stderr or "").strip() or f"git clone failed (rc={proc.returncode})"
|
|
973
|
+
logger.info(f"Git clone error: {err}")
|
|
974
|
+
return False, err
|
|
975
|
+
|
|
976
|
+
if not os.path.isdir(target_dir) or not any(Path(target_dir).iterdir()):
|
|
977
|
+
logger.info(f"No files found in {target_dir} after clone.")
|
|
978
|
+
return False, "No files found after clone."
|
|
979
|
+
|
|
980
|
+
# Clone finished within the first wait (or later): still enforce size_limit_gb
|
|
981
|
+
if limit is not None:
|
|
982
|
+
current = _dir_size_bytes(target_dir)
|
|
983
|
+
if current >= limit:
|
|
984
|
+
return _fail_for_size("clone completed", current, kill_proc=False)
|
|
985
|
+
|
|
986
|
+
return True, ""
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def download_git_repository(
|
|
990
|
+
refs_to_checkout,
|
|
991
|
+
git_url,
|
|
992
|
+
target_dir,
|
|
993
|
+
tag="",
|
|
994
|
+
called_cli=True,
|
|
995
|
+
size_limit_gb: Optional[float] = None,
|
|
996
|
+
credential_id: str = "",
|
|
997
|
+
git_token: str = "",
|
|
998
|
+
):
|
|
684
999
|
success = False
|
|
685
1000
|
oss_version = ""
|
|
1001
|
+
msg = ""
|
|
686
1002
|
|
|
687
1003
|
logger.info(f"Download git url :{git_url}, version:{refs_to_checkout}")
|
|
1004
|
+
|
|
1005
|
+
# Avoid hard process exit from parent watchdog while size-guarded clone may run longer
|
|
1006
|
+
try:
|
|
1007
|
+
_cancel_download_watchdog()
|
|
1008
|
+
except Exception:
|
|
1009
|
+
pass
|
|
1010
|
+
|
|
688
1011
|
env = os.environ.copy()
|
|
689
1012
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
690
1013
|
if platform.system() == "Windows":
|
|
@@ -692,7 +1015,6 @@ def download_git_repository(refs_to_checkout, git_url, target_dir, tag, called_c
|
|
|
692
1015
|
else:
|
|
693
1016
|
env["GIT_ASKPASS"] = "/bin/echo"
|
|
694
1017
|
env["GIT_CREDENTIAL_HELPER"] = ""
|
|
695
|
-
# Disable credential helper via config
|
|
696
1018
|
if "GIT_CONFIG_COUNT" not in env:
|
|
697
1019
|
env["GIT_CONFIG_COUNT"] = "1"
|
|
698
1020
|
env["GIT_CONFIG_KEY_0"] = "credential.helper"
|
|
@@ -702,74 +1024,48 @@ def download_git_repository(refs_to_checkout, git_url, target_dir, tag, called_c
|
|
|
702
1024
|
else:
|
|
703
1025
|
env["GIT_SSH_COMMAND"] = env["GIT_SSH_COMMAND"] + " -o BatchMode=yes"
|
|
704
1026
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
# Use subprocess to ensure environment variables are properly passed
|
|
710
|
-
cmd = ["git", "-c", "credential.helper=", "-c", "credential.helper=!", "clone", git_url, target_dir]
|
|
711
|
-
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=600, stdin=subprocess.DEVNULL)
|
|
712
|
-
if result.returncode == 0:
|
|
713
|
-
# Checkout the specific branch or tag
|
|
714
|
-
checkout_cmd = ["git", "-C", target_dir, "checkout", refs_to_checkout]
|
|
715
|
-
checkout_result = subprocess.run(
|
|
716
|
-
checkout_cmd, env=env, capture_output=True, text=True,
|
|
717
|
-
timeout=60, stdin=subprocess.DEVNULL)
|
|
718
|
-
if checkout_result.returncode == 0:
|
|
719
|
-
if any(Path(target_dir).iterdir()):
|
|
720
|
-
success = True
|
|
721
|
-
oss_version = refs_to_checkout
|
|
722
|
-
logger.info(f"Files found in {target_dir} after clone and checkout.")
|
|
723
|
-
else:
|
|
724
|
-
logger.info(f"No files found in {target_dir} after clone.")
|
|
725
|
-
success = False
|
|
726
|
-
else:
|
|
727
|
-
logger.info(f"Git checkout error: {checkout_result.stderr}")
|
|
728
|
-
# Clone succeeded but checkout failed (e.g. non-existent ref):
|
|
729
|
-
# repo has default branch; treat as success with empty version
|
|
730
|
-
if any(Path(target_dir).iterdir()):
|
|
731
|
-
success = True
|
|
732
|
-
oss_version = ""
|
|
733
|
-
logger.info("Checkout failed; keeping default branch.")
|
|
734
|
-
else:
|
|
735
|
-
logger.info(f"Git clone error: {result.stderr}")
|
|
736
|
-
success = False
|
|
737
|
-
except subprocess.TimeoutExpired:
|
|
738
|
-
logger.info("Git clone timeout")
|
|
739
|
-
success = False
|
|
740
|
-
except Exception as e:
|
|
741
|
-
logger.info(f"Git clone error:{e}")
|
|
742
|
-
success = False
|
|
1027
|
+
# If target already exists from a failed attempt, empty it before clone
|
|
1028
|
+
if os.path.isdir(target_dir) and any(Path(target_dir).iterdir()):
|
|
1029
|
+
_cleanup_target_dir(target_dir)
|
|
1030
|
+
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
743
1031
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1032
|
+
cmd = build_shallow_clone_cmd(git_url, target_dir, refs_to_checkout)
|
|
1033
|
+
logger.info(f"Shallow clone cmd: {' '.join(cmd)}")
|
|
1034
|
+
ok, err = run_git_clone_with_size_guard(
|
|
1035
|
+
cmd, env, target_dir, size_limit_gb=size_limit_gb
|
|
1036
|
+
)
|
|
1037
|
+
if ok:
|
|
1038
|
+
success = True
|
|
1039
|
+
oss_version = refs_to_checkout or ""
|
|
1040
|
+
logger.info(f"Files found in {target_dir} after shallow clone.")
|
|
1041
|
+
return success, oss_version, msg
|
|
1042
|
+
|
|
1043
|
+
msg = err
|
|
1044
|
+
# If branch/tag clone failed, fall back to default-branch shallow clone
|
|
1045
|
+
if refs_to_checkout:
|
|
1046
|
+
logger.info(
|
|
1047
|
+
f"Shallow clone with ref '{refs_to_checkout}' failed; "
|
|
1048
|
+
"retrying default branch shallow clone."
|
|
1049
|
+
)
|
|
1050
|
+
_cleanup_target_dir(target_dir)
|
|
1051
|
+
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
1052
|
+
fallback_cmd = build_shallow_clone_cmd(git_url, target_dir, "")
|
|
1053
|
+
ok2, err2 = run_git_clone_with_size_guard(
|
|
1054
|
+
fallback_cmd, env, target_dir, size_limit_gb=size_limit_gb
|
|
1055
|
+
)
|
|
1056
|
+
if ok2:
|
|
1057
|
+
success = True
|
|
1058
|
+
oss_version = ""
|
|
1059
|
+
msg = ""
|
|
1060
|
+
logger.info("Fallback shallow clone succeeded (default branch).")
|
|
1061
|
+
return success, oss_version, msg
|
|
1062
|
+
msg = err2 or err
|
|
1063
|
+
return success, oss_version, msg
|
|
769
1064
|
|
|
770
1065
|
|
|
771
1066
|
def download_git_clone(git_url, target_dir, checkout_to="", tag="", branch="",
|
|
772
|
-
ssh_key="", id="", git_token="", called_cli=True
|
|
1067
|
+
ssh_key="", id="", git_token="", called_cli=True,
|
|
1068
|
+
size_limit_gb: Optional[float] = None):
|
|
773
1069
|
oss_name = get_github_ossname(git_url)
|
|
774
1070
|
refs_to_checkout, decided_clarified = _resolve_refs_to_checkout(
|
|
775
1071
|
checkout_to, tag, branch, git_url, credential_id=id, git_token=git_token
|
|
@@ -777,14 +1073,10 @@ def download_git_clone(git_url, target_dir, checkout_to="", tag="", branch="",
|
|
|
777
1073
|
msg = ""
|
|
778
1074
|
success = True
|
|
779
1075
|
oss_version = ""
|
|
1076
|
+
alarm = None
|
|
780
1077
|
|
|
781
1078
|
try:
|
|
782
|
-
|
|
783
|
-
signal.signal(signal.SIGALRM, alarm_handler)
|
|
784
|
-
signal.alarm(SIGNAL_TIMEOUT)
|
|
785
|
-
else:
|
|
786
|
-
alarm = Alarm(SIGNAL_TIMEOUT)
|
|
787
|
-
alarm.start()
|
|
1079
|
+
alarm = _start_download_watchdog()
|
|
788
1080
|
|
|
789
1081
|
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
790
1082
|
|
|
@@ -796,22 +1088,29 @@ def download_git_clone(git_url, target_dir, checkout_to="", tag="", branch="",
|
|
|
796
1088
|
logger.info(f"Download git with ssh_key:{git_url}")
|
|
797
1089
|
git_ssh_cmd = f'ssh -i {ssh_key}'
|
|
798
1090
|
with Git().custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
799
|
-
success, oss_version = download_git_repository(
|
|
1091
|
+
success, oss_version, repo_msg = download_git_repository(
|
|
1092
|
+
refs_to_checkout, git_url, target_dir, tag, called_cli,
|
|
1093
|
+
size_limit_gb=size_limit_gb,
|
|
1094
|
+
credential_id=id, git_token=git_token,
|
|
1095
|
+
)
|
|
800
1096
|
else:
|
|
801
1097
|
git_url = _git_url_with_http_credentials(git_url, credential_id=id, git_token=git_token)
|
|
802
|
-
success, oss_version = download_git_repository(
|
|
1098
|
+
success, oss_version, repo_msg = download_git_repository(
|
|
1099
|
+
refs_to_checkout, git_url, target_dir, tag, called_cli,
|
|
1100
|
+
size_limit_gb=size_limit_gb,
|
|
1101
|
+
credential_id=id, git_token=git_token,
|
|
1102
|
+
)
|
|
1103
|
+
if repo_msg and not success:
|
|
1104
|
+
msg = repo_msg
|
|
803
1105
|
|
|
804
1106
|
logger.info(f"git checkout version: {oss_version}")
|
|
805
1107
|
refs_to_checkout = oss_version
|
|
806
|
-
|
|
807
|
-
if platform.system() != "Windows":
|
|
808
|
-
signal.alarm(0)
|
|
809
|
-
else:
|
|
810
|
-
del alarm
|
|
811
1108
|
except Exception as error:
|
|
812
1109
|
success = False
|
|
813
1110
|
logger.warning(f"git clone - failed: {error}")
|
|
814
1111
|
msg = str(error)
|
|
1112
|
+
finally:
|
|
1113
|
+
_cancel_download_watchdog(alarm)
|
|
815
1114
|
|
|
816
1115
|
if oss_version:
|
|
817
1116
|
clarified_version = decided_clarified or clarified_version_from_oss_version(
|
|
@@ -822,21 +1121,153 @@ def download_git_clone(git_url, target_dir, checkout_to="", tag="", branch="",
|
|
|
822
1121
|
return success, msg, oss_name, refs_to_checkout, clarified_version
|
|
823
1122
|
|
|
824
1123
|
|
|
825
|
-
def
|
|
1124
|
+
def _cleanup_new_paths(directory, before_paths):
|
|
1125
|
+
"""Remove files/dirs under ``directory`` that were not in ``before_paths``."""
|
|
1126
|
+
root = Path(directory)
|
|
1127
|
+
if not root.is_dir():
|
|
1128
|
+
return
|
|
1129
|
+
keep = {Path(p).resolve() for p in before_paths}
|
|
1130
|
+
for entry in list(root.iterdir()):
|
|
1131
|
+
try:
|
|
1132
|
+
if entry.resolve() in keep:
|
|
1133
|
+
continue
|
|
1134
|
+
except OSError:
|
|
1135
|
+
pass
|
|
1136
|
+
try:
|
|
1137
|
+
if entry.is_file() or entry.is_symlink():
|
|
1138
|
+
entry.unlink(missing_ok=True)
|
|
1139
|
+
elif entry.is_dir():
|
|
1140
|
+
shutil.rmtree(entry, ignore_errors=True)
|
|
1141
|
+
except OSError:
|
|
1142
|
+
continue
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _download_with_system_wget(url, target_dir, size_limit_gb=None):
|
|
1146
|
+
"""
|
|
1147
|
+
Download ``url`` with the system ``wget`` binary.
|
|
1148
|
+
|
|
1149
|
+
precheck Content-Length, poll directory size while wget runs, then
|
|
1150
|
+
enforce final file size. Raises SizeLimitExceeded on overflow.
|
|
1151
|
+
Returns local file path on success, or None on failure.
|
|
1152
|
+
"""
|
|
1153
|
+
if not shutil.which("wget"):
|
|
1154
|
+
logger.warning("system wget not found; skip wget fallback")
|
|
1155
|
+
return None
|
|
1156
|
+
|
|
1157
|
+
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
1158
|
+
|
|
1159
|
+
# Before download: best-effort Content-Length check
|
|
1160
|
+
try:
|
|
1161
|
+
head = requests.head(url, timeout=10, allow_redirects=True)
|
|
1162
|
+
if head.status_code < 400:
|
|
1163
|
+
_raise_if_content_length_over_limit(
|
|
1164
|
+
head.headers, size_limit_gb, "before download"
|
|
1165
|
+
)
|
|
1166
|
+
except SizeLimitExceeded:
|
|
1167
|
+
raise
|
|
1168
|
+
except Exception:
|
|
1169
|
+
pass
|
|
1170
|
+
|
|
1171
|
+
before = set()
|
|
1172
|
+
try:
|
|
1173
|
+
before = {p.resolve() for p in Path(target_dir).iterdir()}
|
|
1174
|
+
except OSError:
|
|
1175
|
+
before = set()
|
|
1176
|
+
|
|
1177
|
+
try:
|
|
1178
|
+
proc = subprocess.Popen(
|
|
1179
|
+
[
|
|
1180
|
+
"wget",
|
|
1181
|
+
"-nv",
|
|
1182
|
+
"--tries=20",
|
|
1183
|
+
"--timeout=60",
|
|
1184
|
+
"--waitretry=5",
|
|
1185
|
+
"-c",
|
|
1186
|
+
"-P", str(target_dir),
|
|
1187
|
+
url,
|
|
1188
|
+
],
|
|
1189
|
+
stdout=subprocess.DEVNULL,
|
|
1190
|
+
stderr=subprocess.PIPE,
|
|
1191
|
+
text=True,
|
|
1192
|
+
stdin=subprocess.DEVNULL,
|
|
1193
|
+
)
|
|
1194
|
+
except Exception as error:
|
|
1195
|
+
logger.warning(f"system wget - failed to start: {error}")
|
|
1196
|
+
return None
|
|
1197
|
+
|
|
1198
|
+
limit = _size_limit_bytes(size_limit_gb)
|
|
1199
|
+
while proc.poll() is None:
|
|
1200
|
+
if limit is not None:
|
|
1201
|
+
current = _dir_size_bytes(target_dir)
|
|
1202
|
+
if current >= limit:
|
|
1203
|
+
logger.info(
|
|
1204
|
+
"system wget exceeded size limit during progress "
|
|
1205
|
+
f"({current} bytes >= {limit}); aborting."
|
|
1206
|
+
)
|
|
1207
|
+
proc.terminate()
|
|
1208
|
+
try:
|
|
1209
|
+
proc.wait(timeout=10)
|
|
1210
|
+
except subprocess.TimeoutExpired:
|
|
1211
|
+
proc.kill()
|
|
1212
|
+
try:
|
|
1213
|
+
proc.wait(timeout=5)
|
|
1214
|
+
except Exception:
|
|
1215
|
+
pass
|
|
1216
|
+
# Drain stderr after exit to avoid leaving a blocked pipe reader.
|
|
1217
|
+
try:
|
|
1218
|
+
proc.communicate(timeout=5)
|
|
1219
|
+
except Exception:
|
|
1220
|
+
pass
|
|
1221
|
+
_cleanup_new_paths(target_dir, before)
|
|
1222
|
+
raise SizeLimitExceeded(
|
|
1223
|
+
_size_limit_abort_message(
|
|
1224
|
+
size_limit_gb, "during download", current
|
|
1225
|
+
)
|
|
1226
|
+
)
|
|
1227
|
+
time.sleep(SIZE_CHECK_INTERVAL_SECONDS)
|
|
1228
|
+
|
|
1229
|
+
_stdout, stderr = proc.communicate()
|
|
1230
|
+
if proc.returncode != 0:
|
|
1231
|
+
logger.warning(
|
|
1232
|
+
f"system wget failed (rc={proc.returncode}): {(stderr or '').strip()}"
|
|
1233
|
+
)
|
|
1234
|
+
_cleanup_new_paths(target_dir, before)
|
|
1235
|
+
return None
|
|
1236
|
+
|
|
1237
|
+
try:
|
|
1238
|
+
candidates = [
|
|
1239
|
+
p for p in Path(target_dir).iterdir()
|
|
1240
|
+
if p.is_file() and not p.is_symlink()
|
|
1241
|
+
]
|
|
1242
|
+
except OSError:
|
|
1243
|
+
return None
|
|
1244
|
+
new_files = [p for p in candidates if p.resolve() not in before]
|
|
1245
|
+
pick_from = new_files or candidates
|
|
1246
|
+
if not pick_from:
|
|
1247
|
+
return None
|
|
1248
|
+
local_path = str(max(pick_from, key=lambda p: p.stat().st_size))
|
|
1249
|
+
|
|
1250
|
+
try:
|
|
1251
|
+
_raise_if_bytes_over_limit(
|
|
1252
|
+
os.path.getsize(local_path), size_limit_gb, "after download"
|
|
1253
|
+
)
|
|
1254
|
+
except SizeLimitExceeded:
|
|
1255
|
+
_cleanup_new_paths(target_dir, before)
|
|
1256
|
+
raise
|
|
1257
|
+
return local_path
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def download_wget(link, target_dir, compressed_only, checkout_to, size_limit_gb=None):
|
|
826
1261
|
success = False
|
|
827
1262
|
msg = ""
|
|
828
1263
|
oss_name = ""
|
|
829
1264
|
oss_version = ""
|
|
830
1265
|
downloaded_file = ""
|
|
831
1266
|
resolved_link = ""
|
|
1267
|
+
alarm = None
|
|
832
1268
|
|
|
833
1269
|
try:
|
|
834
|
-
|
|
835
|
-
signal.signal(signal.SIGALRM, alarm_handler)
|
|
836
|
-
signal.alarm(SIGNAL_TIMEOUT)
|
|
837
|
-
else:
|
|
838
|
-
alarm = Alarm(SIGNAL_TIMEOUT)
|
|
839
|
-
alarm.start()
|
|
1270
|
+
alarm = _start_download_watchdog()
|
|
840
1271
|
|
|
841
1272
|
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
842
1273
|
|
|
@@ -876,34 +1307,58 @@ def download_wget(link, target_dir, compressed_only, checkout_to):
|
|
|
876
1307
|
else:
|
|
877
1308
|
raise Exception('Not a downloadable link')
|
|
878
1309
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1310
|
+
# 2차: requests HTTP download
|
|
1311
|
+
logger.info(f"HTTP(requests) download: {link}")
|
|
1312
|
+
try:
|
|
1313
|
+
downloaded_file = download_file(
|
|
1314
|
+
link, target_dir, size_limit_gb=size_limit_gb
|
|
1315
|
+
)
|
|
1316
|
+
except SizeLimitExceeded:
|
|
1317
|
+
raise
|
|
1318
|
+
except Exception as error:
|
|
1319
|
+
logger.warning(
|
|
1320
|
+
f"requests download failed: {error}; trying system wget"
|
|
1321
|
+
)
|
|
1322
|
+
downloaded_file = None
|
|
1323
|
+
|
|
1324
|
+
# 3차: system wget fallback (former scancode fetch_http wget path)
|
|
1325
|
+
if not downloaded_file:
|
|
1326
|
+
logger.info(f"system wget fallback: {link}")
|
|
1327
|
+
downloaded_file = _download_with_system_wget(
|
|
1328
|
+
link, target_dir, size_limit_gb=size_limit_gb
|
|
1329
|
+
)
|
|
885
1330
|
|
|
886
1331
|
if downloaded_file:
|
|
1332
|
+
_reject_empty_or_html_download(downloaded_file, link)
|
|
887
1333
|
success = True
|
|
888
1334
|
hint = _oss_version_hint_from_wget_link(link, downloaded_file)
|
|
889
1335
|
# Keep version resolved by get_downloadable_url() when present.
|
|
890
1336
|
# File-name hint is only a fallback when no version was detected.
|
|
891
1337
|
if hint and not oss_version:
|
|
892
1338
|
oss_version = hint
|
|
893
|
-
logger.debug(f"wget - downloaded: {downloaded_file}")
|
|
1339
|
+
logger.debug(f"HTTP/wget - downloaded: {downloaded_file}")
|
|
894
1340
|
else:
|
|
895
1341
|
success = False
|
|
896
1342
|
msg = f"Download failed: {link}"
|
|
897
|
-
logger.warning(
|
|
1343
|
+
logger.warning(
|
|
1344
|
+
"HTTP/wget - failed: no file after requests and system wget: %s",
|
|
1345
|
+
link,
|
|
1346
|
+
)
|
|
1347
|
+
except SizeLimitExceeded as error:
|
|
1348
|
+
success = False
|
|
1349
|
+
msg = str(error)
|
|
1350
|
+
logger.warning(f"HTTP/wget - size limit: {error}")
|
|
898
1351
|
except Exception as error:
|
|
899
1352
|
success = False
|
|
900
1353
|
msg = str(error)
|
|
901
|
-
logger.warning(f"wget - failed: {error}")
|
|
1354
|
+
logger.warning(f"HTTP/wget - failed: {error}")
|
|
1355
|
+
finally:
|
|
1356
|
+
_cancel_download_watchdog(alarm)
|
|
902
1357
|
|
|
903
1358
|
return success, downloaded_file, msg, oss_name, oss_version, resolved_link
|
|
904
1359
|
|
|
905
1360
|
|
|
906
|
-
def _download_file_once(url, target_dir, request_headers=None):
|
|
1361
|
+
def _download_file_once(url, target_dir, request_headers=None, size_limit_gb=None):
|
|
907
1362
|
"""One HTTP download attempt. Raises requests.HTTPError on HTTP failure."""
|
|
908
1363
|
final_url = url
|
|
909
1364
|
head_headers = {}
|
|
@@ -916,57 +1371,113 @@ def _download_file_once(url, target_dir, request_headers=None):
|
|
|
916
1371
|
if h.status_code >= 400:
|
|
917
1372
|
final_url = url
|
|
918
1373
|
head_headers = {}
|
|
1374
|
+
else:
|
|
1375
|
+
# Before download: Content-Length size check
|
|
1376
|
+
_raise_if_content_length_over_limit(
|
|
1377
|
+
head_headers, size_limit_gb, "before download"
|
|
1378
|
+
)
|
|
1379
|
+
except SizeLimitExceeded:
|
|
1380
|
+
raise
|
|
919
1381
|
except Exception:
|
|
920
1382
|
final_url = url
|
|
921
1383
|
head_headers = {}
|
|
922
1384
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
"
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1385
|
+
local_path = None
|
|
1386
|
+
try:
|
|
1387
|
+
with requests.get(
|
|
1388
|
+
final_url,
|
|
1389
|
+
stream=True,
|
|
1390
|
+
allow_redirects=True,
|
|
1391
|
+
timeout=SIGNAL_TIMEOUT,
|
|
1392
|
+
headers=request_headers,
|
|
1393
|
+
) as r:
|
|
1394
|
+
r.raise_for_status()
|
|
1395
|
+
# GET headers may also expose Content-Length
|
|
1396
|
+
_raise_if_content_length_over_limit(
|
|
1397
|
+
r.headers, size_limit_gb, "before download"
|
|
1398
|
+
)
|
|
1399
|
+
content_type = (r.headers.get("content-type") or "").lower()
|
|
1400
|
+
if "text/html" in content_type and _url_looks_like_binary_archive(url):
|
|
1401
|
+
raise _HtmlWhenBinaryExpected()
|
|
1402
|
+
|
|
1403
|
+
filename = ""
|
|
1404
|
+
cd = r.headers.get("Content-Disposition") or head_headers.get(
|
|
1405
|
+
"Content-Disposition"
|
|
1406
|
+
)
|
|
1407
|
+
if cd:
|
|
1408
|
+
m_star = re.search(r"filename\*=(?:UTF-8'')?([^;\r\n]+)", cd)
|
|
1409
|
+
if m_star:
|
|
1410
|
+
filename = urllib.parse.unquote(m_star.group(1).strip('"\''))
|
|
1411
|
+
else:
|
|
1412
|
+
m = re.search(r"filename=([^;\r\n]+)", cd)
|
|
1413
|
+
if m:
|
|
1414
|
+
filename = m.group(1).strip('"\'')
|
|
950
1415
|
if not filename:
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1416
|
+
final_for_name = r.url or final_url
|
|
1417
|
+
filename = os.path.basename(urllib.parse.urlparse(final_for_name).path)
|
|
1418
|
+
if not filename:
|
|
1419
|
+
filename = "downloaded_file"
|
|
1420
|
+
filename = os.path.basename(filename) or "downloaded_file"
|
|
1421
|
+
|
|
1422
|
+
if os.path.isdir(target_dir):
|
|
1423
|
+
local_path = os.path.join(target_dir, filename)
|
|
1424
|
+
else:
|
|
1425
|
+
local_path = target_dir
|
|
1426
|
+
|
|
1427
|
+
written = 0
|
|
1428
|
+
with open(local_path, 'wb') as f:
|
|
1429
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
1430
|
+
if not chunk:
|
|
1431
|
+
continue
|
|
1432
|
+
f.write(chunk)
|
|
1433
|
+
written += len(chunk)
|
|
1434
|
+
# During download: abort as soon as written bytes exceed limit
|
|
1435
|
+
try:
|
|
1436
|
+
_raise_if_bytes_over_limit(
|
|
1437
|
+
written, size_limit_gb, "during download"
|
|
1438
|
+
)
|
|
1439
|
+
except SizeLimitExceeded:
|
|
1440
|
+
f.close()
|
|
1441
|
+
try:
|
|
1442
|
+
os.remove(local_path)
|
|
1443
|
+
except OSError:
|
|
1444
|
+
pass
|
|
1445
|
+
raise
|
|
1446
|
+
except SizeLimitExceeded:
|
|
1447
|
+
if local_path:
|
|
1448
|
+
try:
|
|
1449
|
+
if os.path.exists(local_path):
|
|
1450
|
+
os.remove(local_path)
|
|
1451
|
+
except OSError:
|
|
1452
|
+
pass
|
|
1453
|
+
raise
|
|
956
1454
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1455
|
+
# After download: final file size check
|
|
1456
|
+
if local_path and os.path.exists(local_path):
|
|
1457
|
+
try:
|
|
1458
|
+
_raise_if_bytes_over_limit(
|
|
1459
|
+
os.path.getsize(local_path), size_limit_gb, "after download"
|
|
1460
|
+
)
|
|
1461
|
+
except SizeLimitExceeded:
|
|
1462
|
+
try:
|
|
1463
|
+
os.remove(local_path)
|
|
1464
|
+
except OSError:
|
|
1465
|
+
pass
|
|
1466
|
+
raise
|
|
960
1467
|
return local_path
|
|
961
1468
|
|
|
962
1469
|
|
|
963
|
-
def download_file(url, target_dir):
|
|
1470
|
+
def download_file(url, target_dir, size_limit_gb=None):
|
|
964
1471
|
"""Download via HTTP(S); retry on 403 or HTML interstitial for archive URLs."""
|
|
965
1472
|
attempts = _download_http_header_attempts()
|
|
966
1473
|
last_i = len(attempts) - 1
|
|
967
1474
|
for i, req_headers in enumerate(attempts):
|
|
968
1475
|
try:
|
|
969
|
-
return _download_file_once(
|
|
1476
|
+
return _download_file_once(
|
|
1477
|
+
url, target_dir, req_headers, size_limit_gb=size_limit_gb
|
|
1478
|
+
)
|
|
1479
|
+
except SizeLimitExceeded:
|
|
1480
|
+
raise
|
|
970
1481
|
except requests.exceptions.HTTPError as e:
|
|
971
1482
|
if (
|
|
972
1483
|
e.response is not None
|
|
@@ -1318,6 +1829,8 @@ def main():
|
|
|
1318
1829
|
parser.add_argument('-z', '--compressed_only', help='Unzip only compressed file',
|
|
1319
1830
|
action='store_true', dest='compressed_only', default=False)
|
|
1320
1831
|
parser.add_argument('-o', '--output', help='Generate output file', action='store_true', dest='output', default=False)
|
|
1832
|
+
parser.add_argument('-l', '--size-limit', help='Max download size in GB (omit for unlimited)',
|
|
1833
|
+
type=float, dest='size_limit', default=None)
|
|
1321
1834
|
|
|
1322
1835
|
src_link = ""
|
|
1323
1836
|
target_dir = os.getcwd()
|
|
@@ -1325,6 +1838,7 @@ def main():
|
|
|
1325
1838
|
checkout_to = ""
|
|
1326
1839
|
compressed_only = False
|
|
1327
1840
|
output = False
|
|
1841
|
+
size_limit_gb = None
|
|
1328
1842
|
|
|
1329
1843
|
try:
|
|
1330
1844
|
args = parser.parse_args()
|
|
@@ -1345,13 +1859,15 @@ def main():
|
|
|
1345
1859
|
compressed_only = args.compressed_only
|
|
1346
1860
|
if args.output:
|
|
1347
1861
|
output = args.output
|
|
1862
|
+
if args.size_limit is not None:
|
|
1863
|
+
size_limit_gb = args.size_limit
|
|
1348
1864
|
|
|
1349
1865
|
if not src_link:
|
|
1350
1866
|
print_help_msg_download()
|
|
1351
1867
|
else:
|
|
1352
1868
|
cli_download_and_extract(src_link, target_dir, log_dir, checkout_to,
|
|
1353
1869
|
compressed_only, "", "", "", False,
|
|
1354
|
-
output)
|
|
1870
|
+
output, size_limit_gb=size_limit_gb)
|
|
1355
1871
|
|
|
1356
1872
|
|
|
1357
1873
|
if __name__ == '__main__':
|
|
@@ -69,6 +69,7 @@ _HELP_MESSAGE_DOWNLOAD = """
|
|
|
69
69
|
-t <path> Output directory to save the downloaded files
|
|
70
70
|
-d <log_dir> Directory to save the log file
|
|
71
71
|
-c <branch/tag> Checkout to branch, tag, or version after download
|
|
72
|
+
-l <GB> Max download size in GB (omit for unlimited)
|
|
72
73
|
-z Unzip only compressed file
|
|
73
74
|
-o Generate summary output file
|
|
74
75
|
-h Show this help message
|
|
@@ -81,6 +82,9 @@ _HELP_MESSAGE_DOWNLOAD = """
|
|
|
81
82
|
# Download and checkout to a specific branch
|
|
82
83
|
fosslight_download -s https://github.com/fosslight/fosslight -t output_dir -c develop
|
|
83
84
|
|
|
85
|
+
# Limit download size to 3 GB
|
|
86
|
+
fosslight_download -s https://github.com/fosslight/fosslight -t output_dir -l 3
|
|
87
|
+
|
|
84
88
|
# Download and unzip a compressed file
|
|
85
89
|
fosslight_download -s https://example.com/archive.zip -z -t output_dir
|
|
86
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fosslight_util
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: FOSSLight Util
|
|
5
5
|
Author: LG Electronics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -165,6 +165,10 @@ If you give a link, the source is downloaded to the target directory through git
|
|
|
165
165
|
```
|
|
166
166
|
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/
|
|
167
167
|
```
|
|
168
|
+
Limit download size (GB) with `--size-limit` / `-l` (omit for unlimited):
|
|
169
|
+
```
|
|
170
|
+
$ fosslight_download -s "https://github.com/LGE-OSS/example" -t target_dir/ -l 5
|
|
171
|
+
```
|
|
168
172
|
If you want to try with private repository, set your github token like below.
|
|
169
173
|
```
|
|
170
174
|
$ fosslight_download -s "https://my_github_token@github.com/Foo/private_repo -t target_dir/"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/frequentLicenselist.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|