fix(paths): centralize file:// URL handling to fix broken save-dir paths

This commit is contained in:
2026-07-13 15:45:22 -07:00
parent 44216e5857
commit d03b889d88
9 changed files with 42 additions and 35 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ Popup {
id: saveDirDialog
title: "Choose a folder to save data"
onAccepted: {
var dir = decodeURIComponent(String(selectedFolder).replace(/^file:\/\//, ""))
var dir = String(selectedFolder)
deviceController.setSaveDataDir(dir)
file_browser.setCurrentDirectory(dir)
root.finished()
+1 -5
View File
@@ -171,10 +171,6 @@ Rectangle {
}
}
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
}
function _doDetect() {
root.memoryRead = false
root.waferDetected = false
@@ -187,7 +183,7 @@ Rectangle {
id: saveDirDialog
title: "Choose a folder to save Data"
onAccepted: {
var dir = root.cleanFolderUrl(selectedFolder)
var dir = String(selectedFolder)
deviceController.setSaveDataDir(dir)
file_browser.setCurrentDirectory(dir)
root._doDetect()
+2 -5
View File
@@ -113,9 +113,6 @@ ColumnLayout {
readonly property bool isBusy: deviceController.connectionStatus.endsWith("...")
readonly property string portName: deviceController.selectedPort || "—"
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
}
function currentFamilyCode() {
var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : "";
@@ -128,7 +125,7 @@ ColumnLayout {
id: saveDirDialog
title: "Choose a folder to save."
onAccepted: {
var dir = root.cleanFolderUrl(selectedFolder);
var dir = String(selectedFolder);
deviceController.setSaveDataDir(dir);
file_browser.setCurrentDirectory(dir);
root.parseAndSavePendingRead();
@@ -141,7 +138,7 @@ ColumnLayout {
id: dirOnlyDialog
title: "Choose a folder to save Data"
onAccepted: {
var dir = root.cleanFolderUrl(selectedFolder);
var dir = String(selectedFolder);
deviceController.setSaveDataDir(dir);
file_browser.setCurrentDirectory(dir);
}
@@ -260,14 +260,14 @@ ColumnLayout {
id: importFileDialog
title: "Select Data File To Import"
nameFilters: ["CSV files (*.csv)"]
onAccepted: file_browser.importZWaferCsv(String(selectedFile).replace(/^file:\/\//, ""))
onAccepted: file_browser.importZWaferCsv(String(selectedFile))
}
FolderDialog {
id: batchExportDirDialog
title: "Export Wafer Maps To…"
onAccepted: {
var dir = String(selectedFolder).replace(/^file:\/\//, "");
var dir = String(selectedFolder);
var paths = file_browser.files.map(function(row) { return row.fileName; });
batchExportController.start(paths, dir);
}
@@ -5,7 +5,7 @@ import threading
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
from pygui.backend.utils import slot_error_boundary
from pygui.backend.utils import slot_error_boundary, to_local_path
from pygui.backend.wafer.batch_export import BatchExportResult, export_batch
log = logging.getLogger(__name__)
@@ -33,7 +33,9 @@ class BatchExportController(QObject):
self._busy = True
self.busyChanged.emit()
threading.Thread(
target=self._worker, args=(list(file_paths), output_dir), daemon=True
target=self._worker,
args=(list(file_paths), str(to_local_path(output_dir))),
daemon=True,
).start()
def _worker(self, file_paths: list[str], output_dir: str) -> None:
@@ -15,7 +15,7 @@ from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
from pygui.backend.data.local_settings import LocalSettings
from pygui.backend.models.data_model import TemperatureTableModel
from pygui.backend.utils import slot_error_boundary
from pygui.backend.utils import slot_error_boundary, to_local_path
from pygui.backend.visualization.graph_view import GraphView
from pygui.serialcomm.data_parser import (
convert_to_debug_temperatures,
@@ -82,7 +82,8 @@ class DeviceController(QObject):
self._operation_in_progress = False
self._activity_log: list[str] = []
self._raw_bytes: Optional[bytes] = None
self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv")
saved_dir = getattr(settings, 'save_data_dir', "")
self._save_data_dir: str = str(to_local_path(saved_dir)) if saved_dir else str(Path(self._data_dir) / "csv")
self._last_wafer_info: dict[str, Any] = {}
self._wafer_detected: bool = False
self._last_csv_path: str = ""
@@ -141,8 +142,8 @@ class DeviceController(QObject):
@slot_error_boundary
def setSaveDataDir(self, path: str) -> None:
"""Set the directory for saving parsed CSV data files."""
self._save_data_dir = path
self._append_log(f"Save data dir set to: {path}")
self._save_data_dir = str(to_local_path(path))
self._append_log(f"Save data dir set to: {self._save_data_dir}")
self._save_status()
@Slot(result=str)
+4 -2
View File
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import QFileDialog, QMessageBox
from pygui.backend.data.constants import default_data_dir
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
from pygui.backend.utils import to_local_path
from pygui.backend.wafer.zwafer_parser import ZWaferParser
@@ -45,7 +46,7 @@ class FileBrowser(QObject):
Used to keep this directory in sync with DeviceController.saveDataDir
when the user picks a save folder elsewhere in the app.
"""
self._set_current_directory(Path(path))
self._set_current_directory(to_local_path(path))
self._refresh_files(show_empty_message=False)
@Slot()
@@ -124,7 +125,8 @@ class FileBrowser(QObject):
date still gets a deterministic (if degenerate) filename, so the
duplicate-import guard below still fires on a second attempt.
"""
source_path = Path(file_path)
source_path = to_local_path(file_path)
file_path = str(source_path)
parser_data, _ = self._parser.parse(file_path)
if parser_data is None:
self.importFinished.emit("", f"Failed to parse CSV:\n{source_path.name}")
+2 -14
View File
@@ -5,7 +5,7 @@ import shutil
import time
from pathlib import Path
from PySide6.QtCore import Property, QObject, QUrl, Signal, Slot
from PySide6.QtCore import Property, QObject, Signal, Slot
from pygui.backend.license.license_manager import (
LICENSE_FILENAME_RE,
@@ -13,25 +13,13 @@ from pygui.backend.license.license_manager import (
parse_license_blob,
read_license_files,
)
from pygui.backend.utils import to_local_path as _to_local_path
log = logging.getLogger(__name__)
TRIAL_DAYS = 30
def _to_local_path(path_or_url: str) -> Path:
"""Accept both plain paths and file:// URLs (QML FileDialog gives URLs).
Uses QUrl.toLocalFile() rather than urlparse — a manual
urlparse().path strip leaves a leading slash before a Windows drive
letter (e.g. "/C:/Users/...", not a valid absolute path), which
QUrl's platform-aware conversion handles correctly.
"""
if path_or_url.startswith("file:"):
return Path(QUrl(path_or_url).toLocalFile())
return Path(path_or_url)
# ===== License Model =====
class LicenseModel(QObject):
"""Context property backing the About dialog license grid.
+21
View File
@@ -1,5 +1,26 @@
import logging
import re
from functools import wraps
from pathlib import Path
from PySide6.QtCore import QUrl
_MANGLED_DRIVE_RE = re.compile(r"^/([A-Za-z]:)")
def to_local_path(path_or_url: str) -> Path:
"""Accept plain paths, file:// URLs, and already-mangled "/C:/..." strings
(persisted by old buggy code before it stripped file:// by hand).
Uses QUrl.toLocalFile() rather than a manual string strip — stripping
"file://" by hand leaves a leading slash before a Windows drive letter
(e.g. "/C:/Users/...", not a valid absolute path), which QUrl's
platform-aware conversion handles correctly.
"""
if path_or_url.startswith("file:"):
return Path(QUrl(path_or_url).toLocalFile())
# ponytail: repair values saved by the old regex-stripping QML code
return Path(_MANGLED_DRIVE_RE.sub(r"\1", path_or_url))
def slot_error_boundary(func):