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.
Files changed (51) hide show
  1. {fosslight_util-2.2.3/src/fosslight_util.egg-info → fosslight_util-2.2.4}/PKG-INFO +5 -1
  2. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/README.md +4 -0
  3. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/pyproject.toml +1 -1
  4. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/download.py +660 -144
  5. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/help.py +4 -0
  6. {fosslight_util-2.2.3 → fosslight_util-2.2.4/src/fosslight_util.egg-info}/PKG-INFO +5 -1
  7. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/LICENSE +0 -0
  8. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/MANIFEST.in +0 -0
  9. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/setup.cfg +0 -0
  10. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/__init__.py +0 -0
  11. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/_get_downloadable_url.py +0 -0
  12. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/compare_yaml.py +0 -0
  13. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/constant.py +0 -0
  14. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/correct.py +0 -0
  15. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/cover.py +0 -0
  16. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/exclude.py +0 -0
  17. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/get_pom_license.py +0 -0
  18. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/oss_item.py +0 -0
  19. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/output_format.py +0 -0
  20. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/parsing_yaml.py +0 -0
  21. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/read_excel.py +0 -0
  22. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/frequentLicenselist.json +0 -0
  23. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/frequent_license_nick_list.json +0 -0
  24. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/resources/licenses.json +0 -0
  25. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/set_log.py +0 -0
  26. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/spdx_licenses.py +0 -0
  27. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/time.py +0 -0
  28. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/timer_thread.py +0 -0
  29. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_cyclonedx.py +0 -0
  30. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_excel.py +0 -0
  31. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_opossum.py +0 -0
  32. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_scancodejson.py +0 -0
  33. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_spdx.py +0 -0
  34. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_txt.py +0 -0
  35. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util/write_yaml.py +0 -0
  36. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/SOURCES.txt +0 -0
  37. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/dependency_links.txt +0 -0
  38. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/entry_points.txt +0 -0
  39. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/requires.txt +0 -0
  40. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/src/fosslight_util.egg-info/top_level.txt +0 -0
  41. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_cyclonedx.py +0 -0
  42. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_decide_checkout.py +0 -0
  43. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download.py +0 -0
  44. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download_packagist.py +0 -0
  45. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_download_version_hint.py +0 -0
  46. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_opossum.py +0 -0
  47. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_spdx_licenses.py +0 -0
  48. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_text.py +0 -0
  49. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_write_excel_hidden_sheet.py +0 -0
  50. {fosslight_util-2.2.3 → fosslight_util-2.2.4}/tests/test_write_output.py +0 -0
  51. {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
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/"
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_util"
10
- version = "2.2.3"
10
+ version = "2.2.4"
11
11
  description = "FOSSLight Util"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -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.setDaemon(True)
97
+ self._cancelled = threading.Event()
98
+ self.daemon = True
81
99
 
82
100
  def run(self):
83
- time.sleep(self.timeout)
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) -> Tuple[bool, str, str, str, str]:
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 (git clone, wget)
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
- if (not is_rubygems) and (not success_git):
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 success and downloaded_file:
219
- success = extract_compressed_file(downloaded_file, target_dir, True, compressed_only)
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 msg:
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 download_git_repository(refs_to_checkout, git_url, target_dir, tag, called_cli=True):
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
- if refs_to_checkout:
706
- try:
707
- # For tags, we need full history. For branches, shallow clone is possible but
708
- # we use full clone to ensure compatibility with all cases
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
- if not success:
745
- try:
746
- # Use subprocess to ensure environment variables are properly passed
747
- # No checkout needed, so shallow clone is sufficient
748
- cmd = [
749
- "git", "-c", "credential.helper=", "-c", "credential.helper=!",
750
- "clone", "--depth", "1", git_url, target_dir
751
- ]
752
- result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=600, stdin=subprocess.DEVNULL)
753
- if result.returncode == 0:
754
- if any(Path(target_dir).iterdir()):
755
- success = True
756
- else:
757
- logger.info(f"No files found in {target_dir} after clone.")
758
- success = False
759
- else:
760
- logger.info(f"Git clone error: {result.stderr}")
761
- success = False
762
- except subprocess.TimeoutExpired:
763
- logger.info("Git clone timeout")
764
- success = False
765
- except Exception as e:
766
- logger.info(f"Git clone error:{e}")
767
- success = False
768
- return success, oss_version
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
- if platform.system() != "Windows":
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(refs_to_checkout, git_url, target_dir, tag, called_cli)
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(refs_to_checkout, git_url, target_dir, tag, called_cli)
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 download_wget(link, target_dir, compressed_only, checkout_to):
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
- if platform.system() != "Windows":
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
- logger.info(f"wget: {link}")
880
- downloaded_file = download_file(link, target_dir)
881
- if platform.system() != "Windows":
882
- signal.alarm(0)
883
- else:
884
- del alarm
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("wget - failed: download_file returned no file: %s", link)
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
- with requests.get(
924
- final_url,
925
- stream=True,
926
- allow_redirects=True,
927
- timeout=SIGNAL_TIMEOUT,
928
- headers=request_headers,
929
- ) as r:
930
- r.raise_for_status()
931
- content_type = (r.headers.get("content-type") or "").lower()
932
- if "text/html" in content_type and _url_looks_like_binary_archive(url):
933
- raise _HtmlWhenBinaryExpected()
934
-
935
- filename = ""
936
- cd = r.headers.get("Content-Disposition") or head_headers.get(
937
- "Content-Disposition"
938
- )
939
- if cd:
940
- m_star = re.search(r"filename\*=(?:UTF-8'')?([^;\r\n]+)", cd)
941
- if m_star:
942
- filename = urllib.parse.unquote(m_star.group(1).strip('"\''))
943
- else:
944
- m = re.search(r"filename=([^;\r\n]+)", cd)
945
- if m:
946
- filename = m.group(1).strip('"\'')
947
- if not filename:
948
- final_for_name = r.url or final_url
949
- filename = os.path.basename(urllib.parse.urlparse(final_for_name).path)
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
- filename = "downloaded_file"
952
- if os.path.isdir(target_dir):
953
- local_path = os.path.join(target_dir, filename)
954
- else:
955
- local_path = target_dir
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
- with open(local_path, 'wb') as f:
958
- for chunk in r.iter_content(chunk_size=8192):
959
- f.write(chunk)
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(url, target_dir, req_headers)
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
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