|
52 | 52 | CONFIG_SECTION_MAIN = "main" |
53 | 53 | CONFIG_KEY_PATH = "path" |
54 | 54 | CONFIG_KEY_LAST_CHECK = "lastcheck" |
55 | | -CONFIG_KEY_LAST_DL_DESC = "lastdl_desc" |
56 | 55 | CONFIG_KEY_LAST_DL_FILENAME = "lastdl_filename" |
57 | 56 | CONFIG_KEY_INSTALLED_FILENAME = "installed_filename" |
58 | 57 | CONFIG_KEY_OS_FILTER = "os_filter" |
|
92 | 91 | logger = logging.getLogger() |
93 | 92 |
|
94 | 93 |
|
| 94 | +_BUILD_LABEL_RE = re.compile( |
| 95 | + r"blender-(\d+\.\d+[^-]*)-(?:.*-)?(windows|linux|darwin|macos)[\.\-]?(\w+)?", |
| 96 | + re.IGNORECASE, |
| 97 | +) |
| 98 | + |
| 99 | + |
| 100 | +def _build_label(filename: str) -> str: |
| 101 | + """Return a short human-readable label for a Blender build filename.""" |
| 102 | + m = _BUILD_LABEL_RE.search(filename) |
| 103 | + if m: |
| 104 | + version, os_name, arch = m.group(1), m.group(2), m.group(3) |
| 105 | + os_label = OS_DESCRIPTIONS.get(os_name.lower(), os_name) |
| 106 | + return f"Blender {version} ({os_label}{' ' + arch if arch else ''})" |
| 107 | + # Fallback: strip prefix and extension |
| 108 | + stem = re.sub(r"\.(zip|tar\.xz|dmg|tar\.gz)$", "", filename, flags=re.IGNORECASE) |
| 109 | + return stem.removeprefix("blender-") |
| 110 | + |
| 111 | + |
95 | 112 | def _hbytes(num: float) -> str: |
96 | 113 | if not isinstance(num, (int, float)): |
97 | 114 | return "0 bytes" |
@@ -257,6 +274,7 @@ def run(self): |
257 | 274 | downloaded_size = 0 |
258 | 275 | for chunk in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE): |
259 | 276 | if self._cancelled: |
| 277 | + response.close() |
260 | 278 | self.signals.finished.emit(False, CANCEL_MESSAGE) |
261 | 279 | return |
262 | 280 | f.write(chunk) |
@@ -291,8 +309,16 @@ def run(self): |
291 | 309 | extract_dir = os.path.join(self.temp_dir, "extracted") |
292 | 310 | os.makedirs(extract_dir, exist_ok=True) |
293 | 311 | shutil.unpack_archive(self.download_path, extract_dir) |
294 | | - source_dir_name = next(os.walk(extract_dir))[1][0] |
295 | | - source_path = os.path.join(extract_dir, source_dir_name) |
| 312 | + |
| 313 | + # Zip-slip guard: ensure no extracted path escapes the extract directory |
| 314 | + extract_dir_resolved = Path(extract_dir).resolve() |
| 315 | + for member in extract_dir_resolved.rglob("*"): |
| 316 | + member.resolve().relative_to(extract_dir_resolved) |
| 317 | + |
| 318 | + _, subdirs, _ = next(os.walk(extract_dir)) |
| 319 | + if not subdirs: |
| 320 | + raise OSError("Archive did not contain a top-level directory as expected") |
| 321 | + source_path = os.path.join(extract_dir, subdirs[0]) |
296 | 322 |
|
297 | 323 | # Copying |
298 | 324 | self.signals.copying_started.emit() |
@@ -327,6 +353,7 @@ def __init__(self, parent=None): |
327 | 353 | r'blender-\d+\.\d+[^"\s/]*\.(?:zip|tar\.xz|dmg|tar\.gz)' |
328 | 354 | ) |
329 | 355 | self.threadpool = QThreadPool() |
| 356 | + self.finallist = [] |
330 | 357 | logger.info( |
331 | 358 | f"Multithreading with maximum {self.threadpool.maxThreadCount()} threads" |
332 | 359 | ) |
@@ -372,10 +399,11 @@ def __init__(self, parent=None): |
372 | 399 | self.line_path.setText(self.install_path) |
373 | 400 |
|
374 | 401 | last_dl_filename = self._get_config(CONFIG_KEY_LAST_DL_FILENAME) |
375 | | - last_dl_desc = self._get_config(CONFIG_KEY_LAST_DL_DESC) |
376 | | - if last_dl_filename and last_dl_desc: |
377 | | - self.btn_oneclick.setText(f"{last_dl_filename} | {last_dl_desc}") |
| 402 | + if last_dl_filename: |
| 403 | + self.btn_oneclick.setText(_build_label(last_dl_filename)) |
| 404 | + self.btn_oneclick.clicked.connect(lambda: self.download(last_dl_filename)) |
378 | 405 | self.btn_oneclick.show() |
| 406 | + self.lbl_quick.show() |
379 | 407 |
|
380 | 408 | self.btn_cancel.hide() |
381 | 409 | self.frm_progress.hide() |
@@ -408,7 +436,7 @@ def __init__(self, parent=None): |
408 | 436 | logger.critical("No internet connection") |
409 | 437 |
|
410 | 438 | try: |
411 | | - response = self.session.get(GITHUB_RELEASES_API_URL) |
| 439 | + response = self.session.get(GITHUB_RELEASES_API_URL, timeout=CONNECTIVITY_TIMEOUT) |
412 | 440 | response.raise_for_status() |
413 | 441 | update_data = response.json() |
414 | 442 | logger.info("Reading existing configuration file") |
@@ -479,7 +507,6 @@ def _load_config(self): |
479 | 507 | defaults = { |
480 | 508 | CONFIG_KEY_PATH: "", |
481 | 509 | CONFIG_KEY_LAST_CHECK: "Never", |
482 | | - CONFIG_KEY_LAST_DL_DESC: "", |
483 | 510 | CONFIG_KEY_LAST_DL_FILENAME: "", |
484 | 511 | CONFIG_KEY_INSTALLED_FILENAME: "", |
485 | 512 | CONFIG_KEY_OS_FILTER: "all", |
@@ -587,6 +614,12 @@ def on_check_finished(self, build_list, error): |
587 | 614 |
|
588 | 615 | self.finallist = build_list |
589 | 616 |
|
| 617 | + last_dl_filename = self._get_config(CONFIG_KEY_LAST_DL_FILENAME) |
| 618 | + if last_dl_filename and last_dl_filename not in build_list: |
| 619 | + self._update_config(CONFIG_KEY_LAST_DL_FILENAME, "") |
| 620 | + self.btn_oneclick.hide() |
| 621 | + self.lbl_quick.hide() |
| 622 | + |
590 | 623 | self.appleicon = QtGui.QIcon(":/newPrefix/images/Apple-icon.png") |
591 | 624 | self.windowsicon = QtGui.QIcon(":/newPrefix/images/Windows-icon.png") |
592 | 625 | self.linuxicon = QtGui.QIcon(":/newPrefix/images/Linux-icon.png") |
@@ -766,9 +799,13 @@ def done(self, installed_entry_filename): |
766 | 799 | elif opsys == "Linux": |
767 | 800 | self.btn_execute.clicked.connect(self.exec_linux) |
768 | 801 |
|
769 | | - last_dl_desc = self._get_config(CONFIG_KEY_LAST_DL_DESC) |
770 | | - if last_dl_desc and installed_entry_filename: |
771 | | - self.btn_oneclick.setText(f"{installed_entry_filename} | {last_dl_desc}") |
| 802 | + if installed_entry_filename: |
| 803 | + self.btn_oneclick.setText(_build_label(installed_entry_filename)) |
| 804 | + try: |
| 805 | + self.btn_oneclick.clicked.disconnect() |
| 806 | + except RuntimeError: |
| 807 | + pass |
| 808 | + self.btn_oneclick.clicked.connect(lambda: self.download(installed_entry_filename)) |
772 | 809 | self.btn_oneclick.show() |
773 | 810 | self.lbl_quick.show() |
774 | 811 | else: |
|
0 commit comments