From 749ffb7e34ab6701a1ac5bc292b3072573d8ab1d Mon Sep 17 00:00:00 2001 From: Nose Date: Mon, 12 Jan 2026 20:10:18 -0800 Subject: [PATCH] f --- API/folder.py | 24 ++++++++++++++ SYS/config.py | 55 +++++++++++++++++++++++++++++++++ TUI/modalscreen/config_modal.py | 15 +++++---- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/API/folder.py b/API/folder.py index ea27a4b..8fbeaf1 100644 --- a/API/folder.py +++ b/API/folder.py @@ -263,7 +263,31 @@ class API_folder_store: try: # Ensure the library root exists; sqlite cannot create parent dirs. try: + # User safety: Folder store must be created in a blank folder/no files in it. + # If the DB already exists, we skip this check (it's an existing library). + should_check_empty = not self.db_path.exists() + self.library_root.mkdir(parents=True, exist_ok=True) + + if should_check_empty: + # Check if there are any files or directories in the library root (excluding the DB itself if it was just created) + # We use a generator and next() for efficiency. + existing_items = [item for item in self.library_root.iterdir() if item.name != self.DB_NAME] + if existing_items: + # Log the items found for debugging + item_names = [i.name for i in existing_items[:5]] + if len(existing_items) > 5: + item_names.append("...") + raise RuntimeError( + f"Safety Check Failed: Local library root must be empty for new stores.\n" + f"Directory: {self.library_root}\n" + f"Found {len(existing_items)} items: {', '.join(item_names)}\n" + f"Please use a clean directory to prevent accidental hashing of existing files." + ) + + except RuntimeError: + # Re-raise our specific safety error + raise except Exception as exc: raise RuntimeError( f"Cannot create/open library root directory: {self.library_root}: {exc}" diff --git a/SYS/config.py b/SYS/config.py index 1c408c7..515600e 100644 --- a/SYS/config.py +++ b/SYS/config.py @@ -693,6 +693,58 @@ def clear_config_cache() -> None: _CONFIG_CACHE.clear() +def _validate_config_safety(config: Dict[str, Any]) -> None: + """Check for dangerous configurations, like folder stores in non-empty dirs.""" + store = config.get("store") + if not isinstance(store, dict): + return + + folder_stores = store.get("folder") + if not isinstance(folder_stores, dict): + return + + for name, cfg in folder_stores.items(): + if not isinstance(cfg, dict): + continue + + path_str = cfg.get("path") or cfg.get("PATH") + if not path_str: + continue + + try: + p = expand_path(path_str).resolve() + # If the path doesn't exist yet, it's fine (will be created empty) + if not p.exists(): + continue + + if not p.is_dir(): + continue + + # DB name from API/folder.py + db_file = p / "medios-macina.db" + if db_file.exists(): + # Existing portable library, allowed to re-attach + continue + + # Check if directory has any files (other than the DB we just checked) + items = list(p.iterdir()) + if items: + item_names = [i.name for i in items[:3]] + if len(items) > 3: + item_names.append("...") + raise RuntimeError( + f"Configuration Error: Local library '{name}' target directory is not empty.\n" + f"Path: {p}\n" + f"Found {len(items)} items: {', '.join(item_names)}\n" + f"To prevent accidental mass-hashing, new libraries must be set to unique, empty folders." + ) + except RuntimeError: + raise + except Exception: + # We don't want to crash on invalid paths during validation if they aren't 'unsafe' + pass + + def save_config( config: Dict[str, Any], config_dir: Optional[Path] = None, @@ -706,6 +758,9 @@ def save_config( f"Unsupported config format: {config_path.name} (only .conf is supported)" ) + # Safety Check: Validate folder stores are in empty dirs or existing libraries + _validate_config_safety(config) + try: config_path.write_text(_serialize_conf(config), encoding="utf-8") except OSError as exc: diff --git a/TUI/modalscreen/config_modal.py b/TUI/modalscreen/config_modal.py index a80053e..8a58208 100644 --- a/TUI/modalscreen/config_modal.py +++ b/TUI/modalscreen/config_modal.py @@ -450,12 +450,15 @@ class ConfigModal(ModalScreen): elif bid == "save-btn": if not self.validate_current_editor(): return - self.save_all() - self.notify("Configuration saved!") - # Return to the main list view within the current category - self.editing_item_name = None - self.editing_item_type = None - self.refresh_view() + try: + self.save_all() + self.notify("Configuration saved!") + # Return to the main list view within the current category + self.editing_item_name = None + self.editing_item_type = None + self.refresh_view() + except Exception as exc: + self.notify(f"Save failed: {exc}", severity="error", timeout=10) elif bid in self._button_id_map: action, itype, name = self._button_id_map[bid] if action == "edit":