From 9e99b8309107aa46344b175fb23902286441dcc0 Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Sat, 14 Feb 2026 12:50:01 -0600 Subject: [PATCH] fix: default cert browser to home dir instead of /etc/ssl/certs (#203) The CA certificate file browser hardcoded /etc/ssl/certs as fallback, which confused users when the dialog opened to a system directory. Default to the user's home directory instead. --- freecad/silo_commands.py | 182 ++++++++++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 50 deletions(-) diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index bea0aa6..452f0f9 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -26,7 +26,9 @@ from silo_client import ( _PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo" # Configuration - preferences take priority over env vars -SILO_PROJECTS_DIR = os.environ.get("SILO_PROJECTS_DIR", os.path.expanduser("~/projects")) +SILO_PROJECTS_DIR = os.environ.get( + "SILO_PROJECTS_DIR", os.path.expanduser("~/projects") +) # --------------------------------------------------------------------------- @@ -64,7 +66,9 @@ class FreeCADSiloSettings(SiloSettings): param = FreeCAD.ParamGet(_PREF_GROUP) return param.GetString("SslCertPath", "") - def save_auth(self, username: str, role: str = "", source: str = "", token: str = ""): + def save_auth( + self, username: str, role: str = "", source: str = "", token: str = "" + ): param = FreeCAD.ParamGet(_PREF_GROUP) param.SetString("AuthUsername", username) param.SetString("AuthRole", role) @@ -122,7 +126,9 @@ def _get_ssl_verify() -> bool: def _get_ssl_context(): from silo_client._ssl import build_ssl_context - return build_ssl_context(_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path()) + return build_ssl_context( + _fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path() + ) def _get_auth_headers() -> Dict[str, str]: @@ -179,7 +185,9 @@ def _fetch_server_mode() -> str: # Icon helper # --------------------------------------------------------------------------- -_ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icons") +_ICON_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "resources", "icons" +) def _icon(name): @@ -580,7 +588,9 @@ def handle_kindred_url(url: str): parts = [parsed.netloc] + [p for p in parsed.path.split("/") if p] if len(parts) >= 2 and parts[0] == "item": part_number = parts[1] - FreeCAD.Console.PrintMessage(f"Silo: Opening item {part_number} from kindred:// URL\n") + FreeCAD.Console.PrintMessage( + f"Silo: Opening item {part_number} from kindred:// URL\n" + ) _sync.open_item(part_number) @@ -600,9 +610,8 @@ class Silo_Open: } def Activated(self): - from PySide import QtGui, QtWidgets - from open_search import OpenItemWidget + from PySide import QtGui, QtWidgets mw = FreeCADGui.getMainWindow() mdi = mw.findChild(QtWidgets.QMdiArea) @@ -649,7 +658,6 @@ class Silo_New: def Activated(self): from PySide import QtGui, QtWidgets - from schema_form import SchemaFormWidget mw = FreeCADGui.getMainWindow() @@ -689,7 +697,9 @@ class Silo_New: }, ) obj.Label = part_number - _sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True) + _sync.save_to_canonical_path( + FreeCAD.ActiveDocument, force_rename=True + ) else: _sync.create_document_for_item(result, save=True) @@ -770,7 +780,9 @@ class Silo_Save: # Try to upload to MinIO try: - result = _client._upload_file(part_number, str(file_path), properties, "Auto-save") + result = _client._upload_file( + part_number, str(file_path), properties, "Auto-save" + ) new_rev = result["revision_number"] FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n") @@ -803,7 +815,9 @@ class Silo_Commit: obj = get_tracked_object(doc) if not obj: - FreeCAD.Console.PrintError("No tracked object. Use 'New' to register first.\n") + FreeCAD.Console.PrintError( + "No tracked object. Use 'New' to register first.\n" + ) return part_number = obj.SiloPartNumber @@ -820,7 +834,9 @@ class Silo_Commit: if not file_path: return - result = _client._upload_file(part_number, str(file_path), properties, comment) + result = _client._upload_file( + part_number, str(file_path), properties, comment + ) new_rev = result["revision_number"] FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n") @@ -869,7 +885,9 @@ def _check_pull_conflicts(part_number, local_path, doc=None): server_updated = item.get("updated_at", "") if server_updated: # Parse ISO format timestamp - server_dt = datetime.datetime.fromisoformat(server_updated.replace("Z", "+00:00")) + server_dt = datetime.datetime.fromisoformat( + server_updated.replace("Z", "+00:00") + ) if server_dt > local_mtime: conflicts.append("Server version is newer than local file.") except Exception: @@ -899,7 +917,9 @@ class SiloPullDialog: # Revision table self._table = QtGui.QTableWidget() self._table.setColumnCount(5) - self._table.setHorizontalHeaderLabels(["Rev", "Date", "Comment", "Status", "File"]) + self._table.setHorizontalHeaderLabels( + ["Rev", "Date", "Comment", "Status", "File"] + ) self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -992,7 +1012,9 @@ def _pull_dependencies(part_number, progress_callback=None): # Skip if already exists locally existing = find_file_by_part_number(child_pn) if existing and existing.exists(): - FreeCAD.Console.PrintMessage(f" {child_pn}: already exists at {existing}\n") + FreeCAD.Console.PrintMessage( + f" {child_pn}: already exists at {existing}\n" + ) # Still recurse — this child may itself be an assembly with missing deps _pull_dependencies(child_pn, progress_callback) continue @@ -1072,14 +1094,18 @@ class Silo_Pull: if not has_any_file: if existing_local: - FreeCAD.Console.PrintMessage(f"Opening existing local file: {existing_local}\n") + FreeCAD.Console.PrintMessage( + f"Opening existing local file: {existing_local}\n" + ) FreeCAD.openDocument(str(existing_local)) else: try: item = _client.get_item(part_number) new_doc = _sync.create_document_for_item(item, save=True) if new_doc: - FreeCAD.Console.PrintMessage(f"Created local file for {part_number}\n") + FreeCAD.Console.PrintMessage( + f"Created local file for {part_number}\n" + ) else: QtGui.QMessageBox.warning( None, @@ -1166,7 +1192,9 @@ class Silo_Pull: progress.setValue(100) progress.close() if dep_pulled: - FreeCAD.Console.PrintMessage(f"Pulled {len(dep_pulled)} dependency file(s)\n") + FreeCAD.Console.PrintMessage( + f"Pulled {len(dep_pulled)} dependency file(s)\n" + ) # Close existing document if open, then reopen if doc and doc.FileName == str(dest_path): @@ -1221,7 +1249,9 @@ class Silo_Push: server_dt = datetime.fromisoformat( server_time_str.replace("Z", "+00:00") ) - local_dt = datetime.fromtimestamp(local_mtime, tz=timezone.utc) + local_dt = datetime.fromtimestamp( + local_mtime, tz=timezone.utc + ) if local_dt > server_dt: unuploaded.append(lf) else: @@ -1234,7 +1264,9 @@ class Silo_Push: pass # Not in DB, skip if not unuploaded: - QtGui.QMessageBox.information(None, "Push", "All local files are already uploaded.") + QtGui.QMessageBox.information( + None, "Push", "All local files are already uploaded." + ) return msg = f"Found {len(unuploaded)} files to upload:\n\n" @@ -1252,7 +1284,9 @@ class Silo_Push: uploaded = 0 for item in unuploaded: - result = _sync.upload_file(item["part_number"], item["path"], "Synced from local") + result = _sync.upload_file( + item["part_number"], item["path"], "Synced from local" + ) if result: uploaded += 1 @@ -1301,9 +1335,7 @@ class Silo_Info: msg = f"

{part_number}

" msg += f"

Type: {item.get('item_type', '-')}

" msg += f"

Description: {item.get('description', '-')}

" - msg += ( - f"

Projects: {', '.join(project_codes) if project_codes else 'None'}

" - ) + msg += f"

Projects: {', '.join(project_codes) if project_codes else 'None'}

" msg += f"

Current Revision: {item.get('current_revision', 1)}

" msg += f"

Local Revision: {getattr(obj, 'SiloRevision', '-')}

" @@ -1369,7 +1401,9 @@ class Silo_TagProjects: try: # Get current projects for item current_projects = _client.get_item_projects(part_number) - current_codes = {p.get("code", "") for p in current_projects if p.get("code")} + current_codes = { + p.get("code", "") for p in current_projects if p.get("code") + } # Get all available projects all_projects = _client.get_projects() @@ -1480,7 +1514,9 @@ class Silo_Rollback: dialog.setMinimumHeight(300) layout = QtGui.QVBoxLayout(dialog) - label = QtGui.QLabel(f"Select a revision to rollback to (current: Rev {current_rev}):") + label = QtGui.QLabel( + f"Select a revision to rollback to (current: Rev {current_rev}):" + ) layout.addWidget(label) # Revision table @@ -1495,8 +1531,12 @@ class Silo_Rollback: for i, rev in enumerate(prev_revisions): table.setItem(i, 0, QtGui.QTableWidgetItem(str(rev["revision_number"]))) table.setItem(i, 1, QtGui.QTableWidgetItem(rev.get("status", "draft"))) - table.setItem(i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10])) - table.setItem(i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "")) + table.setItem( + i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10]) + ) + table.setItem( + i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "") + ) table.resizeColumnsToContents() layout.addWidget(table) @@ -1522,7 +1562,9 @@ class Silo_Rollback: def on_rollback(): selected = table.selectedItems() if not selected: - QtGui.QMessageBox.warning(dialog, "Rollback", "Please select a revision") + QtGui.QMessageBox.warning( + dialog, "Rollback", "Please select a revision" + ) return selected_rev[0] = int(table.item(selected[0].row(), 0).text()) dialog.accept() @@ -1620,7 +1662,9 @@ class Silo_SetStatus: # Update status _client.update_revision(part_number, rev_num, status=status) - FreeCAD.Console.PrintMessage(f"Updated Rev {rev_num} status to '{status}'\n") + FreeCAD.Console.PrintMessage( + f"Updated Rev {rev_num} status to '{status}'\n" + ) QtGui.QMessageBox.information( None, "Status Updated", f"Revision {rev_num} status set to '{status}'" ) @@ -1684,7 +1728,9 @@ class Silo_Settings: ssl_checkbox.setChecked(param.GetBool("SslVerify", True)) layout.addWidget(ssl_checkbox) - ssl_hint = QtGui.QLabel("Disable only for internal servers with self-signed certificates.") + ssl_hint = QtGui.QLabel( + "Disable only for internal servers with self-signed certificates." + ) ssl_hint.setWordWrap(True) ssl_hint.setStyleSheet("color: #888; font-size: 11px;") layout.addWidget(ssl_hint) @@ -1717,7 +1763,7 @@ class Silo_Settings: path, _ = QtGui.QFileDialog.getOpenFileName( dialog, "Select CA Certificate", - os.path.dirname(cert_input.text()) or "/etc/ssl/certs", + os.path.dirname(cert_input.text()) or os.path.expanduser("~"), "Certificates (*.pem *.crt *.cer);;All Files (*)", ) if path: @@ -1961,7 +2007,9 @@ class Silo_BOM: wu_table = QtGui.QTableWidget() wu_table.setColumnCount(5) - wu_table.setHorizontalHeaderLabels(["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"]) + wu_table.setHorizontalHeaderLabels( + ["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"] + ) wu_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) wu_table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) wu_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -1990,12 +2038,16 @@ class Silo_BOM: bom_table.setItem( row, 1, QtGui.QTableWidgetItem(entry.get("child_description", "")) ) - bom_table.setItem(row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", ""))) + bom_table.setItem( + row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", "")) + ) qty = entry.get("quantity") bom_table.setItem( row, 3, QtGui.QTableWidgetItem(str(qty) if qty is not None else "") ) - bom_table.setItem(row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "")) + bom_table.setItem( + row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "") + ) ref_des = entry.get("reference_designators") or [] bom_table.setItem(row, 5, QtGui.QTableWidgetItem(", ".join(ref_des))) bom_table.setItem( @@ -2017,12 +2069,16 @@ class Silo_BOM: wu_table.setItem( row, 0, QtGui.QTableWidgetItem(entry.get("parent_part_number", "")) ) - wu_table.setItem(row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", ""))) + wu_table.setItem( + row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", "")) + ) qty = entry.get("quantity") wu_table.setItem( row, 2, QtGui.QTableWidgetItem(str(qty) if qty is not None else "") ) - wu_table.setItem(row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "")) + wu_table.setItem( + row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "") + ) ref_des = entry.get("reference_designators") or [] wu_table.setItem(row, 4, QtGui.QTableWidgetItem(", ".join(ref_des))) wu_table.resizeColumnsToContents() @@ -2075,7 +2131,9 @@ class Silo_BOM: try: qty = float(qty_text) except ValueError: - QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.") + QtGui.QMessageBox.warning( + dialog, "BOM", "Quantity must be a number." + ) return unit = unit_input.text().strip() or None @@ -2154,7 +2212,9 @@ class Silo_BOM: try: new_qty = float(qty_text) except ValueError: - QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.") + QtGui.QMessageBox.warning( + dialog, "BOM", "Quantity must be a number." + ) return new_unit = unit_input.text().strip() or None @@ -2178,7 +2238,9 @@ class Silo_BOM: ) load_bom() except Exception as exc: - QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to update entry:\n{exc}") + QtGui.QMessageBox.warning( + dialog, "BOM", f"Failed to update entry:\n{exc}" + ) def on_remove(): selected = bom_table.selectedItems() @@ -2204,7 +2266,9 @@ class Silo_BOM: _client.delete_bom_entry(part_number, child_pn) load_bom() except Exception as exc: - QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to remove entry:\n{exc}") + QtGui.QMessageBox.warning( + dialog, "BOM", f"Failed to remove entry:\n{exc}" + ) add_btn.clicked.connect(on_add) edit_btn.clicked.connect(on_edit) @@ -2243,7 +2307,9 @@ class SiloEventListener(QtCore.QThread): item_updated = QtCore.Signal(str) # part_number revision_created = QtCore.Signal(str, int) # part_number, revision - connection_status = QtCore.Signal(str, int, str) # (status, retry_count, error_message) + connection_status = QtCore.Signal( + str, int, str + ) # (status, retry_count, error_message) server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded" _MAX_RETRIES = 10 @@ -2317,7 +2383,9 @@ class SiloEventListener(QtCore.QThread): req = urllib.request.Request(url, headers=headers, method="GET") try: - self._response = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=90) + self._response = urllib.request.urlopen( + req, context=_get_ssl_context(), timeout=90 + ) except urllib.error.HTTPError as e: if e.code in (404, 501): raise _SSEUnsupported() @@ -2598,7 +2666,9 @@ class SiloAuthDockWidget: self._sse_label.setToolTip("") FreeCAD.Console.PrintMessage("Silo: SSE connected\n") elif status == "disconnected": - self._sse_label.setText(f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})...") + self._sse_label.setText( + f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..." + ) self._sse_label.setStyleSheet("font-size: 11px; color: #FF9800;") self._sse_label.setToolTip(error or "Connection lost") FreeCAD.Console.PrintWarning( @@ -2608,7 +2678,9 @@ class SiloAuthDockWidget: self._sse_label.setText("Disconnected") self._sse_label.setStyleSheet("font-size: 11px; color: #F44336;") self._sse_label.setToolTip(error or "Max retries reached") - FreeCAD.Console.PrintError(f"Silo: SSE gave up after {retry} retries: {error}\n") + FreeCAD.Console.PrintError( + f"Silo: SSE gave up after {retry} retries: {error}\n" + ) elif status == "unsupported": self._sse_label.setText("Not available") self._sse_label.setStyleSheet("font-size: 11px; color: #888;") @@ -2650,10 +2722,14 @@ class SiloAuthDockWidget: self._refresh_activity_panel() def _on_remote_revision(self, part_number, revision): - FreeCAD.Console.PrintMessage(f"Silo: New revision {revision} for {part_number}\n") + FreeCAD.Console.PrintMessage( + f"Silo: New revision {revision} for {part_number}\n" + ) mw = FreeCADGui.getMainWindow() if mw is not None: - mw.statusBar().showMessage(f"Silo: {part_number} rev {revision} available", 5000) + mw.statusBar().showMessage( + f"Silo: {part_number} rev {revision} available", 5000 + ) self._refresh_activity_panel() def _refresh_activity_panel(self): @@ -2719,7 +2795,9 @@ class SiloAuthDockWidget: rev_part = f" \u2013 Rev {rev_num}" if rev_num else "" date_part = f" \u2013 {updated}" if updated else "" local_badge = " \u25cf local" if pn in local_pns else "" - line1 = f"{pn} \u2013 {desc_display}{rev_part}{date_part}{local_badge}" + line1 = ( + f"{pn} \u2013 {desc_display}{rev_part}{date_part}{local_badge}" + ) if comment: line1 += f'\n "{comment}"' @@ -3175,7 +3253,9 @@ class Silo_StartPanel: dock = QtGui.QDockWidget("Silo", mw) dock.setObjectName("SiloStartPanel") dock.setWidget(content.widget) - dock.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea) + dock.setAllowedAreas( + QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea + ) mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock) def IsActive(self): @@ -3209,7 +3289,9 @@ class _DiagWorker(QtCore.QThread): self.result.emit("DNS", False, "no hostname in URL") return try: - addrs = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM) + addrs = socket.getaddrinfo( + hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM + ) first_ip = addrs[0][4][0] if addrs else "?" self.result.emit("DNS", True, f"{hostname} -> {first_ip}") except socket.gaierror as e: