f
This commit is contained in:
145
SYS/config.py
145
SYS/config.py
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Optional, List
|
||||
from SYS.logger import log
|
||||
from SYS.utils import expand_path
|
||||
from SYS.database import db, get_config_all, save_config_value
|
||||
|
||||
DEFAULT_CONFIG_FILENAME = "config.conf"
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
@@ -208,10 +209,9 @@ def parse_conf_text(text: str, *, base: Optional[Dict[str, Any]] = None) -> Dict
|
||||
|
||||
Supported patterns:
|
||||
- Top-level key/value: temp="./temp"
|
||||
- Sections: [store=folder] + name/path lines
|
||||
- Sections: [store=hydrusnetwork] + name/access key/url lines
|
||||
- Sections: [provider=OpenLibrary] + email/password lines
|
||||
- Dotted keys: store.folder.default.path="C:\\Media" (optional)
|
||||
- Dotted keys: store.hydrusnetwork.<name>.url="http://..." (optional)
|
||||
"""
|
||||
config: Dict[str, Any] = dict(base or {})
|
||||
|
||||
@@ -519,7 +519,6 @@ def get_local_storage_path(config: Dict[str, Any]) -> Optional[Path]:
|
||||
"""Get local storage path from config.
|
||||
|
||||
Supports multiple formats:
|
||||
- New: config["store"]["folder"]["any_name"]["path"]
|
||||
- Old: config["storage"]["local"]["path"]
|
||||
- Old: config["Local"]["path"]
|
||||
|
||||
@@ -529,17 +528,6 @@ def get_local_storage_path(config: Dict[str, Any]) -> Optional[Path]:
|
||||
Returns:
|
||||
Path object if found, None otherwise
|
||||
"""
|
||||
# Try new format: iterate all folder stores and use the first valid path found.
|
||||
store = config.get("store", {})
|
||||
if isinstance(store, dict):
|
||||
folder_config = store.get("folder", {})
|
||||
if isinstance(folder_config, dict):
|
||||
for name, inst_cfg in folder_config.items():
|
||||
if isinstance(inst_cfg, dict):
|
||||
p = inst_cfg.get("path") or inst_cfg.get("PATH")
|
||||
if p:
|
||||
return expand_path(p)
|
||||
|
||||
# Fall back to storage.local.path format
|
||||
storage = config.get("storage", {})
|
||||
if isinstance(storage, dict):
|
||||
@@ -686,32 +674,76 @@ def resolve_debug_log(config: Dict[str, Any]) -> Optional[Path]:
|
||||
return path
|
||||
|
||||
|
||||
def migrate_conf_to_db(config: Dict[str, Any]) -> None:
|
||||
"""Migrate the configuration dictionary to the database."""
|
||||
log("Migrating configuration from .conf to database...")
|
||||
for key, value in config.items():
|
||||
if key in ("store", "provider", "tool", "networking"):
|
||||
cat = key
|
||||
sub_dict = value
|
||||
if isinstance(sub_dict, dict):
|
||||
for subtype, subtype_items in sub_dict.items():
|
||||
if isinstance(subtype_items, dict):
|
||||
# For provider/tool/networking, subtype is the name (e.g. alldebrid)
|
||||
# but for store, it's the type (e.g. hydrusnetwork)
|
||||
if cat == "store" and str(subtype).strip().lower() == "folder":
|
||||
continue
|
||||
if cat != "store":
|
||||
for k, v in subtype_items.items():
|
||||
save_config_value(cat, subtype, "", k, v)
|
||||
else:
|
||||
for name, items in subtype_items.items():
|
||||
if isinstance(items, dict):
|
||||
for k, v in items.items():
|
||||
save_config_value(cat, subtype, name, k, v)
|
||||
else:
|
||||
# Global setting
|
||||
save_config_value("global", "", "", key, value)
|
||||
log("Configuration migration complete!")
|
||||
|
||||
|
||||
def load_config(
|
||||
config_dir: Optional[Path] = None, filename: str = DEFAULT_CONFIG_FILENAME
|
||||
) -> Dict[str, Any]:
|
||||
base_dir = config_dir or SCRIPT_DIR
|
||||
config_path = base_dir / filename
|
||||
cache_key = _make_cache_key(config_dir, filename, config_path)
|
||||
|
||||
if cache_key in _CONFIG_CACHE:
|
||||
return _CONFIG_CACHE[cache_key]
|
||||
|
||||
if config_path.suffix.lower() != ".conf":
|
||||
log(f"Unsupported config format: {config_path.name} (only .conf is supported)")
|
||||
_CONFIG_CACHE[cache_key] = {}
|
||||
return {}
|
||||
# 1. Try loading from database first
|
||||
db_config = get_config_all()
|
||||
if db_config:
|
||||
_CONFIG_CACHE[cache_key] = db_config
|
||||
return db_config
|
||||
|
||||
try:
|
||||
data = _load_conf_config(base_dir, config_path)
|
||||
except FileNotFoundError:
|
||||
_CONFIG_CACHE[cache_key] = {}
|
||||
return {}
|
||||
except OSError as exc:
|
||||
log(f"Failed to read {config_path}: {exc}")
|
||||
_CONFIG_CACHE[cache_key] = {}
|
||||
return {}
|
||||
# 2. If DB is empty, try loading from legacy config.conf
|
||||
if config_path.exists():
|
||||
if config_path.suffix.lower() != ".conf":
|
||||
log(f"Unsupported config format: {config_path.name} (only .conf is supported)")
|
||||
return {}
|
||||
|
||||
try:
|
||||
config = _load_conf_config(base_dir, config_path)
|
||||
# Migrate to database
|
||||
migrate_conf_to_db(config)
|
||||
|
||||
# Optional: Rename old config file to mark as migrated
|
||||
try:
|
||||
migrated_path = config_path.with_name(config_path.name + ".migrated")
|
||||
config_path.rename(migrated_path)
|
||||
log(f"Legacy config file renamed to {migrated_path.name}")
|
||||
except Exception as e:
|
||||
log(f"Could not rename legacy config file: {e}")
|
||||
|
||||
_CONFIG_CACHE[cache_key] = data
|
||||
return data
|
||||
_CONFIG_CACHE[cache_key] = config
|
||||
return config
|
||||
except Exception as e:
|
||||
log(f"Failed to load legacy config at {config_path}: {e}")
|
||||
return {}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def reload_config(
|
||||
@@ -723,55 +755,12 @@ def reload_config(
|
||||
|
||||
|
||||
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
|
||||
"""Validate configuration safety.
|
||||
|
||||
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
|
||||
Folder store validation has been removed because the folder store backend
|
||||
is no longer supported.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
def save_config(
|
||||
@@ -787,7 +776,7 @@ 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
|
||||
# Safety Check: placeholder (folder store validation removed)
|
||||
_validate_config_safety(config)
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user