mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 10:31:50 +01:00
fix: add explicit workspace mode resolution for --config (#818)
This commit is contained in:
@@ -7,7 +7,7 @@ import urllib.parse as urllib_parse
|
||||
from datetime import datetime
|
||||
from gettext import gettext as _
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, cast
|
||||
|
||||
import certifi, colorama, nodriver # isort: skip
|
||||
from nodriver.core.connection import ProtocolException
|
||||
@@ -169,17 +169,17 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
|
||||
self.config:Config
|
||||
self.config_file_path = abspath("config.yaml")
|
||||
self.config_explicitly_provided = False
|
||||
|
||||
self.installation_mode:xdg_paths.InstallationMode | None = None
|
||||
self.workspace:xdg_paths.Workspace | None = None
|
||||
self._config_arg:str | None = None
|
||||
self._workspace_mode_arg:xdg_paths.InstallationMode | None = None
|
||||
|
||||
self.categories:dict[str, str] = {}
|
||||
|
||||
self.file_log:loggers.LogFileHandle | None = None
|
||||
log_file_basename = is_frozen() and os.path.splitext(os.path.basename(sys.executable))[0] or self.__module__
|
||||
self.log_file_path:str | None = abspath(f"{log_file_basename}.log")
|
||||
self.log_file_basename = log_file_basename
|
||||
self.log_file_explicitly_provided = False
|
||||
self._log_basename = os.path.splitext(os.path.basename(sys.executable))[0] if is_frozen() else self.__module__
|
||||
self.log_file_path:str | None = abspath(f"{self._log_basename}.log")
|
||||
self._logfile_arg:str | None = None
|
||||
self._logfile_explicitly_provided = False
|
||||
|
||||
self.command = "help"
|
||||
self.ads_selector = "due"
|
||||
@@ -193,70 +193,67 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.file_log = None
|
||||
self.close_browser_session()
|
||||
|
||||
@property
|
||||
def installation_mode_or_portable(self) -> xdg_paths.InstallationMode:
|
||||
return self.installation_mode or "portable"
|
||||
|
||||
def get_version(self) -> str:
|
||||
return __version__
|
||||
|
||||
def finalize_installation_mode(self) -> None:
|
||||
def _workspace_or_raise(self) -> xdg_paths.Workspace:
|
||||
if self.workspace is None:
|
||||
raise AssertionError(_("Workspace must be resolved before command execution"))
|
||||
return self.workspace
|
||||
|
||||
@property
|
||||
def _update_check_state_path(self) -> Path:
|
||||
return self._workspace_or_raise().state_dir / "update_check_state.json"
|
||||
|
||||
def _resolve_workspace(self) -> None:
|
||||
"""
|
||||
Finalize installation mode detection after CLI args are parsed.
|
||||
Must be called after parse_args() to respect --config overrides.
|
||||
Resolve workspace paths after CLI args are parsed.
|
||||
"""
|
||||
if self.command in {"help", "version"}:
|
||||
if self.command in {"help", "version", "create-config"}:
|
||||
return
|
||||
# Check if config_file_path was already customized (by --config or tests)
|
||||
default_portable_config = xdg_paths.get_config_file_path("portable").resolve()
|
||||
config_path = Path(self.config_file_path).resolve() if self.config_file_path else None
|
||||
config_was_customized = self.config_explicitly_provided or (config_path is not None and config_path != default_portable_config)
|
||||
effective_config_arg = self._config_arg
|
||||
effective_workspace_mode = self._workspace_mode_arg
|
||||
if not effective_config_arg:
|
||||
default_config = (Path.cwd() / "config.yaml").resolve()
|
||||
if self.config_file_path and Path(self.config_file_path).resolve() != default_config:
|
||||
effective_config_arg = self.config_file_path
|
||||
if effective_workspace_mode is None:
|
||||
# Backward compatibility for tests/programmatic assignment of config_file_path:
|
||||
# infer a stable default from the configured path location.
|
||||
config_path = Path(self.config_file_path).resolve()
|
||||
xdg_config_dir = xdg_paths.get_xdg_base_dir("config").resolve()
|
||||
effective_workspace_mode = "xdg" if config_path.is_relative_to(xdg_config_dir) else "portable"
|
||||
|
||||
if config_was_customized and self.config_file_path:
|
||||
# Config path was explicitly set - detect mode based on it
|
||||
LOG.debug("Detecting installation mode from explicit config path: %s", self.config_file_path)
|
||||
try:
|
||||
self.workspace = xdg_paths.resolve_workspace(
|
||||
config_arg = effective_config_arg,
|
||||
logfile_arg = self._logfile_arg,
|
||||
workspace_mode = effective_workspace_mode,
|
||||
logfile_explicitly_provided = self._logfile_explicitly_provided,
|
||||
log_basename = self._log_basename,
|
||||
)
|
||||
except ValueError as exc:
|
||||
LOG.error(str(exc))
|
||||
sys.exit(2)
|
||||
|
||||
if config_path is not None and config_path == (Path.cwd() / "config.yaml").resolve():
|
||||
# Explicit path points to CWD config
|
||||
self.installation_mode = "portable"
|
||||
LOG.debug("Explicit config is in CWD, using portable mode")
|
||||
elif config_path is not None and config_path.is_relative_to(xdg_paths.get_xdg_base_dir("config").resolve()):
|
||||
# Explicit path is within XDG config directory
|
||||
self.installation_mode = "xdg"
|
||||
LOG.debug("Explicit config is in XDG directory, using xdg mode")
|
||||
else:
|
||||
# Custom location - default to portable mode (all paths relative to config)
|
||||
self.installation_mode = "portable"
|
||||
LOG.debug("Explicit config is in custom location, defaulting to portable mode")
|
||||
else:
|
||||
# No explicit config - use auto-detection
|
||||
LOG.debug("Detecting installation mode...")
|
||||
self.installation_mode = xdg_paths.detect_installation_mode()
|
||||
xdg_paths.ensure_directory(self.workspace.config_file.parent, "config directory")
|
||||
|
||||
if self.installation_mode is None:
|
||||
# First run - prompt user
|
||||
LOG.info("First run detected, prompting user for installation mode")
|
||||
self.installation_mode = xdg_paths.prompt_installation_mode()
|
||||
self.config_file_path = str(self.workspace.config_file)
|
||||
self.log_file_path = str(self.workspace.log_file) if self.workspace.log_file else None
|
||||
|
||||
# Set config path based on detected mode
|
||||
self.config_file_path = str(xdg_paths.get_config_file_path(self.installation_mode))
|
||||
|
||||
# Set log file path based on mode (unless explicitly overridden via --logfile)
|
||||
using_default_portable_log = (
|
||||
self.log_file_path is not None and Path(self.log_file_path).resolve() == xdg_paths.get_log_file_path(self.log_file_basename, "portable").resolve()
|
||||
)
|
||||
if not self.log_file_explicitly_provided and using_default_portable_log:
|
||||
# Still using default portable path - update to match detected mode
|
||||
self.log_file_path = str(xdg_paths.get_log_file_path(self.log_file_basename, self.installation_mode))
|
||||
LOG.debug("Log file path: %s", self.log_file_path)
|
||||
|
||||
# Log installation mode and config location (INFO level for user visibility)
|
||||
mode_display = "portable (current directory)" if self.installation_mode == "portable" else "system-wide (XDG directories)"
|
||||
LOG.info("Installation mode: %s [%s]", mode_display, self.config_file_path)
|
||||
LOG.info("Config: %s", self.workspace.config_file)
|
||||
LOG.info("Workspace mode: %s", self.workspace.mode)
|
||||
LOG.info("Workspace: %s", self.workspace.config_dir)
|
||||
if loggers.is_debug(LOG):
|
||||
LOG.debug("Log file: %s", self.workspace.log_file)
|
||||
LOG.debug("State dir: %s", self.workspace.state_dir)
|
||||
LOG.debug("Download dir: %s", self.workspace.download_dir)
|
||||
LOG.debug("Browser profile: %s", self.workspace.browser_profile_dir)
|
||||
LOG.debug("Diagnostics dir: %s", self.workspace.diagnostics_dir)
|
||||
|
||||
async def run(self, args:list[str]) -> None:
|
||||
self.parse_args(args)
|
||||
self.finalize_installation_mode()
|
||||
self._resolve_workspace()
|
||||
try:
|
||||
match self.command:
|
||||
case "help":
|
||||
@@ -276,7 +273,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
self.load_ads()
|
||||
LOG.info("############################################")
|
||||
@@ -285,13 +282,13 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
case "update-check":
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates(skip_interval_check = True)
|
||||
case "update-content-hash":
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
self.ads_selector = "all"
|
||||
if ads := self.load_ads(exclude_ads_with_id = False):
|
||||
@@ -304,7 +301,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
|
||||
if not (
|
||||
@@ -347,7 +344,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
if ads := self.load_ads():
|
||||
await self.create_browser_session()
|
||||
@@ -361,7 +358,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.configure_file_logging()
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
|
||||
# Default to all ads if no selector provided
|
||||
@@ -385,7 +382,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.ads_selector = "new"
|
||||
self.load_config()
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self.installation_mode_or_portable)
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
await self.create_browser_session()
|
||||
await self.login()
|
||||
@@ -452,8 +449,9 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
Mit dieser Option können Sie bestimmte Anzeigen-IDs angeben, z. B. "--ads=1,2,3"
|
||||
--force - Alias für '--ads=all'
|
||||
--keep-old - Verhindert das Löschen alter Anzeigen bei erneuter Veröffentlichung
|
||||
--config=<PATH> - Pfad zur YAML- oder JSON-Konfigurationsdatei (STANDARD: ./config.yaml)
|
||||
--logfile=<PATH> - Pfad zur Protokolldatei (STANDARD: ./kleinanzeigen-bot.log)
|
||||
--config=<PATH> - Pfad zur YAML- oder JSON-Konfigurationsdatei (ändert den Workspace-Modus nicht implizit)
|
||||
--workspace-mode=portable|xdg - Überschreibt den Workspace-Modus für diesen Lauf
|
||||
--logfile=<PATH> - Pfad zur Protokolldatei (STANDARD: vom aktiven Workspace-Modus abhängig)
|
||||
--lang=en|de - Anzeigesprache (STANDARD: Systemsprache, wenn unterstützt, sonst Englisch)
|
||||
-v, --verbose - Aktiviert detaillierte Ausgabe – nur nützlich zur Fehlerbehebung
|
||||
""".rstrip()
|
||||
@@ -504,8 +502,9 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
Use this option to specify ad IDs, e.g. "--ads=1,2,3"
|
||||
--force - alias for '--ads=all'
|
||||
--keep-old - don't delete old ads on republication
|
||||
--config=<PATH> - path to the config YAML or JSON file (DEFAULT: ./config.yaml)
|
||||
--logfile=<PATH> - path to the logfile (DEFAULT: ./kleinanzeigen-bot.log)
|
||||
--config=<PATH> - path to the config YAML or JSON file (does not implicitly change workspace mode)
|
||||
--workspace-mode=portable|xdg - overrides workspace mode for this run
|
||||
--logfile=<PATH> - path to the logfile (DEFAULT: depends on active workspace mode)
|
||||
--lang=en|de - display language (STANDARD: system language if supported, otherwise English)
|
||||
-v, --verbose - enables verbose output - only useful when troubleshooting issues
|
||||
""".rstrip()
|
||||
@@ -514,7 +513,11 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
|
||||
def parse_args(self, args:list[str]) -> None:
|
||||
try:
|
||||
options, arguments = getopt.gnu_getopt(args[1:], "hv", ["ads=", "config=", "force", "help", "keep-old", "logfile=", "lang=", "verbose"])
|
||||
options, arguments = getopt.gnu_getopt(
|
||||
args[1:],
|
||||
"hv",
|
||||
["ads=", "config=", "force", "help", "keep-old", "logfile=", "lang=", "verbose", "workspace-mode="],
|
||||
)
|
||||
except getopt.error as ex:
|
||||
LOG.error(ex.msg)
|
||||
LOG.error("Use --help to display available options.")
|
||||
@@ -527,13 +530,20 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
sys.exit(0)
|
||||
case "--config":
|
||||
self.config_file_path = abspath(value)
|
||||
self.config_explicitly_provided = True
|
||||
self._config_arg = value
|
||||
case "--logfile":
|
||||
if value:
|
||||
self.log_file_path = abspath(value)
|
||||
else:
|
||||
self.log_file_path = None
|
||||
self.log_file_explicitly_provided = True
|
||||
self._logfile_arg = value
|
||||
self._logfile_explicitly_provided = True
|
||||
case "--workspace-mode":
|
||||
mode = value.strip().lower()
|
||||
if mode not in {"portable", "xdg"}:
|
||||
LOG.error("Invalid --workspace-mode '%s'. Use 'portable' or 'xdg'.", value)
|
||||
sys.exit(2)
|
||||
self._workspace_mode_arg = cast(xdg_paths.InstallationMode, mode)
|
||||
case "--ads":
|
||||
self.ads_selector = value.strip().lower()
|
||||
case "--force":
|
||||
@@ -561,6 +571,9 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
if self.file_log:
|
||||
return
|
||||
|
||||
if self.workspace and self.workspace.log_file:
|
||||
xdg_paths.ensure_directory(self.workspace.log_file.parent, "log directory")
|
||||
|
||||
LOG.info("Logging to [%s]...", self.log_file_path)
|
||||
self.file_log = loggers.configure_file_logging(self.log_file_path)
|
||||
|
||||
@@ -575,15 +588,21 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
if os.path.exists(self.config_file_path):
|
||||
LOG.error("Config file %s already exists. Aborting creation.", self.config_file_path)
|
||||
return
|
||||
config_parent = self.workspace.config_file.parent if self.workspace else Path(self.config_file_path).parent
|
||||
xdg_paths.ensure_directory(config_parent, "config directory")
|
||||
default_config = Config.model_construct()
|
||||
default_config.login.username = "changeme" # noqa: S105 placeholder for default config, not a real username
|
||||
default_config.login.password = "changeme" # noqa: S105 placeholder for default config, not a real password
|
||||
dicts.save_commented_model(
|
||||
self.config_file_path,
|
||||
default_config,
|
||||
header = ("# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/config.schema.json"),
|
||||
header = (
|
||||
"# yaml-language-server: "
|
||||
"$schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/config.schema.json"
|
||||
),
|
||||
exclude = {
|
||||
"ad_defaults": {"description"}},
|
||||
"ad_defaults": {"description"},
|
||||
},
|
||||
)
|
||||
|
||||
def load_config(self) -> None:
|
||||
@@ -617,6 +636,8 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
self.browser_config.use_private_window = self.config.browser.use_private_window
|
||||
if self.config.browser.user_data_dir:
|
||||
self.browser_config.user_data_dir = abspath(self.config.browser.user_data_dir, relative_to = self.config_file_path)
|
||||
elif self.workspace:
|
||||
self.browser_config.user_data_dir = str(self.workspace.browser_profile_dir)
|
||||
self.browser_config.profile_name = self.config.browser.profile_name
|
||||
|
||||
def __check_ad_republication(self, ad_cfg:Ad, ad_file_relative:str) -> bool:
|
||||
@@ -962,10 +983,9 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
if diagnostics is not None and diagnostics.output_dir and diagnostics.output_dir.strip():
|
||||
return Path(abspath(diagnostics.output_dir, relative_to = self.config_file_path)).resolve()
|
||||
|
||||
if self.installation_mode_or_portable == "xdg":
|
||||
return xdg_paths.get_xdg_base_dir("cache") / "diagnostics"
|
||||
|
||||
return (Path.cwd() / ".temp" / "diagnostics").resolve()
|
||||
workspace = self._workspace_or_raise()
|
||||
xdg_paths.ensure_directory(workspace.diagnostics_dir, "diagnostics directory")
|
||||
return workspace.diagnostics_dir
|
||||
|
||||
async def _capture_login_detection_diagnostics_if_enabled(self) -> None:
|
||||
cfg = getattr(self.config, "diagnostics", None)
|
||||
@@ -2031,7 +2051,9 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
LOG.warning("Skipping ad with non-numeric id: %s", published_ad.get("id"))
|
||||
LOG.info("Loaded %s published ads.", len(published_ads_by_id))
|
||||
|
||||
ad_extractor = extract.AdExtractor(self.browser, self.config, self.installation_mode_or_portable, published_ads_by_id = published_ads_by_id)
|
||||
workspace = self._workspace_or_raise()
|
||||
xdg_paths.ensure_directory(workspace.download_dir, "downloaded ads directory")
|
||||
ad_extractor = extract.AdExtractor(self.browser, self.config, workspace.download_dir, published_ads_by_id = published_ads_by_id)
|
||||
|
||||
# use relevant download routine
|
||||
if self.ads_selector in {"all", "new"}: # explore ads overview for these two modes
|
||||
|
||||
@@ -15,7 +15,7 @@ from kleinanzeigen_bot.model.ad_model import ContactPartial
|
||||
|
||||
from .model.ad_model import AdPartial
|
||||
from .model.config_model import Config
|
||||
from .utils import dicts, files, i18n, loggers, misc, reflect, xdg_paths
|
||||
from .utils import dicts, files, i18n, loggers, misc, reflect
|
||||
from .utils.web_scraping_mixin import Browser, By, Element, WebScrapingMixin
|
||||
|
||||
__all__ = [
|
||||
@@ -37,15 +37,13 @@ class AdExtractor(WebScrapingMixin):
|
||||
self,
|
||||
browser:Browser,
|
||||
config:Config,
|
||||
installation_mode:xdg_paths.InstallationMode = "portable",
|
||||
download_dir:Path,
|
||||
published_ads_by_id:dict[int, dict[str, Any]] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.browser = browser
|
||||
self.config:Config = config
|
||||
if installation_mode not in {"portable", "xdg"}:
|
||||
raise ValueError(f"Unsupported installation mode: {installation_mode}")
|
||||
self.installation_mode:xdg_paths.InstallationMode = installation_mode
|
||||
self.download_dir = download_dir
|
||||
self.published_ads_by_id:dict[int, dict[str, Any]] = published_ads_by_id or {}
|
||||
|
||||
async def download_ad(self, ad_id:int) -> None:
|
||||
@@ -56,10 +54,8 @@ class AdExtractor(WebScrapingMixin):
|
||||
:param ad_id: the ad ID
|
||||
"""
|
||||
|
||||
# create sub-directory for ad(s) to download (if necessary):
|
||||
download_dir = xdg_paths.get_downloaded_ads_path(self.installation_mode)
|
||||
download_dir = self.download_dir
|
||||
LOG.info("Using download directory: %s", download_dir)
|
||||
# Note: xdg_paths.get_downloaded_ads_path() already creates the directory
|
||||
|
||||
# Extract ad info and determine final directory path
|
||||
ad_cfg, final_dir = await self._extract_ad_page_info_with_directory_handling(download_dir, ad_id)
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from kleinanzeigen_bot.utils import dicts, loggers, misc
|
||||
from kleinanzeigen_bot.utils import dicts, loggers, misc, xdg_paths
|
||||
from kleinanzeigen_bot.utils.pydantics import ContextualModel
|
||||
|
||||
LOG = loggers.get_logger(__name__)
|
||||
@@ -117,6 +117,7 @@ class UpdateCheckState(ContextualModel):
|
||||
if data["last_check"].tzinfo != datetime.timezone.utc:
|
||||
data["last_check"] = data["last_check"].astimezone(datetime.timezone.utc)
|
||||
data["last_check"] = data["last_check"].isoformat()
|
||||
xdg_paths.ensure_directory(state_file.parent, "update check state directory")
|
||||
dicts.save_dict(str(state_file), data)
|
||||
except PermissionError:
|
||||
LOG.warning("Permission denied when saving update check state to %s", state_file)
|
||||
|
||||
@@ -25,6 +25,8 @@ kleinanzeigen_bot/__init__.py:
|
||||
"Direct execution not supported. Use 'pdm run app'": "Direkte Ausführung nicht unterstützt. Bitte 'pdm run app' verwenden"
|
||||
create_default_config:
|
||||
"Config file %s already exists. Aborting creation.": "Konfigurationsdatei %s existiert bereits. Erstellung abgebrochen."
|
||||
_workspace_or_raise:
|
||||
"Workspace must be resolved before command execution": "Arbeitsbereich muss vor der Befehlsausführung aufgelöst werden"
|
||||
|
||||
configure_file_logging:
|
||||
"Logging to [%s]...": "Protokollierung in [%s]..."
|
||||
@@ -140,9 +142,15 @@ kleinanzeigen_bot/__init__.py:
|
||||
find_and_click_extend_button:
|
||||
"Found extend button on page %s": "'Verlängern'-Button auf Seite %s gefunden"
|
||||
|
||||
finalize_installation_mode:
|
||||
"First run detected, prompting user for installation mode": "Erster Start erkannt, frage Benutzer nach Installationsmodus"
|
||||
"Installation mode: %s [%s]": "Installationsmodus: %s [%s]"
|
||||
_resolve_workspace:
|
||||
"Config: %s": "Konfiguration: %s"
|
||||
"Workspace mode: %s": "Arbeitsmodus: %s"
|
||||
"Workspace: %s": "Arbeitsverzeichnis: %s"
|
||||
|
||||
parse_args:
|
||||
"Use --help to display available options.": "Mit --help können die verfügbaren Optionen angezeigt werden."
|
||||
"More than one command given: %s": "Mehr als ein Befehl angegeben: %s"
|
||||
"Invalid --workspace-mode '%s'. Use 'portable' or 'xdg'.": "Ungültiger --workspace-mode '%s'. Verwenden Sie 'portable' oder 'xdg'."
|
||||
|
||||
publish_ads:
|
||||
"Processing %s/%s: '%s' from [%s]...": "Verarbeite %s/%s: '%s' von [%s]..."
|
||||
@@ -241,10 +249,6 @@ kleinanzeigen_bot/__init__.py:
|
||||
"Downloaded ad with id %d": "Anzeige mit der ID %d heruntergeladen"
|
||||
"The page with the id %d does not exist!": "Die Seite mit der ID %d existiert nicht!"
|
||||
|
||||
parse_args:
|
||||
"Use --help to display available options.": "Mit --help können die verfügbaren Optionen angezeigt werden."
|
||||
"More than one command given: %s": "Mehr als ein Befehl angegeben: %s"
|
||||
|
||||
run:
|
||||
"DONE: No configuration errors found.": "FERTIG: Keine Konfigurationsfehler gefunden."
|
||||
"DONE: No active ads found.": "FERTIG: Keine aktiven Anzeigen gefunden."
|
||||
@@ -684,7 +688,7 @@ kleinanzeigen_bot/utils/diagnostics.py:
|
||||
#################################################
|
||||
kleinanzeigen_bot/utils/xdg_paths.py:
|
||||
#################################################
|
||||
_ensure_directory:
|
||||
ensure_directory:
|
||||
"Failed to create %s %s: %s": "Fehler beim Erstellen von %s %s: %s"
|
||||
detect_installation_mode:
|
||||
"Detected installation mode: %s": "Erkannter Installationsmodus: %s"
|
||||
@@ -693,8 +697,18 @@ kleinanzeigen_bot/utils/xdg_paths.py:
|
||||
"Non-interactive mode detected, defaulting to portable installation": "Nicht-interaktiver Modus erkannt, Standard-Installation: portabel"
|
||||
"Choose installation type:": "Installationstyp wählen:"
|
||||
"[1] Portable (current directory)": "[1] Portabel (aktuelles Verzeichnis)"
|
||||
"[2] System-wide (XDG directories)": "[2] Systemweit (XDG-Verzeichnisse)"
|
||||
"[2] User directories (per-user standard locations)": "[2] Benutzerverzeichnisse (pro Benutzer, standardisierte Pfade)"
|
||||
"Enter 1 or 2: ": "1 oder 2 eingeben: "
|
||||
"Defaulting to portable installation mode": "Standard-Installationsmodus: portabel"
|
||||
"User selected installation mode: %s": "Benutzer hat Installationsmodus gewählt: %s"
|
||||
"Invalid choice. Please enter 1 or 2.": "Ungültige Auswahl. Bitte 1 oder 2 eingeben."
|
||||
resolve_workspace: {}
|
||||
_format_hits:
|
||||
"none": "keine"
|
||||
_workspace_mode_resolution_error:
|
||||
? "Cannot determine workspace mode for --config=%(config_file)s. Use --workspace-mode=portable or --workspace-mode=xdg.\nFor cleanup guidance, see: %(url)s"
|
||||
: "Arbeitsmodus für --config=%(config_file)s konnte nicht bestimmt werden. Verwende --workspace-mode=portable oder --workspace-mode=xdg.\nHinweise zur Bereinigung: %(url)s"
|
||||
"Portable footprint hits": "Gefundene portable Spuren"
|
||||
"XDG footprint hits": "Gefundene XDG-Spuren"
|
||||
"Detected both portable and XDG footprints.": "Sowohl portable als auch XDG-Spuren wurden gefunden."
|
||||
"Detected neither portable nor XDG footprints.": "Weder portable noch XDG-Spuren wurden gefunden."
|
||||
|
||||
@@ -12,6 +12,8 @@ import colorama
|
||||
import requests
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from kleinanzeigen_bot.model.config_model import Config
|
||||
|
||||
try:
|
||||
@@ -20,7 +22,6 @@ except ImportError:
|
||||
__version__ = "unknown"
|
||||
|
||||
from kleinanzeigen_bot.model.update_check_state import UpdateCheckState
|
||||
from kleinanzeigen_bot.utils import xdg_paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,16 +31,15 @@ colorama.init()
|
||||
class UpdateChecker:
|
||||
"""Checks for updates to the bot."""
|
||||
|
||||
def __init__(self, config:"Config", installation_mode:str | xdg_paths.InstallationMode = "portable") -> None:
|
||||
def __init__(self, config:"Config", state_file:"Path") -> None:
|
||||
"""Initialize the update checker.
|
||||
|
||||
Args:
|
||||
config: The bot configuration.
|
||||
installation_mode: Installation mode (portable/xdg).
|
||||
state_file: Path to the update-check state JSON file.
|
||||
"""
|
||||
self.config = config
|
||||
self.state_file = xdg_paths.get_update_check_state_path(installation_mode)
|
||||
# Note: xdg_paths handles directory creation
|
||||
self.state_file = state_file
|
||||
self.state = UpdateCheckState.load(self.state_file)
|
||||
|
||||
def get_local_version(self) -> str | None:
|
||||
|
||||
@@ -152,11 +152,6 @@ class WebScrapingMixin:
|
||||
self._default_timeout_config:TimeoutConfig | None = None
|
||||
self.config:BotConfig = cast(BotConfig, None)
|
||||
|
||||
@property
|
||||
def _installation_mode(self) -> str:
|
||||
"""Get installation mode with fallback to portable."""
|
||||
return getattr(self, "installation_mode_or_portable", "portable")
|
||||
|
||||
def _get_timeout_config(self) -> TimeoutConfig:
|
||||
config = getattr(self, "config", None)
|
||||
timeouts:TimeoutConfig | None = None
|
||||
@@ -225,7 +220,7 @@ class WebScrapingMixin:
|
||||
and not has_remote_debugging
|
||||
and not is_test_environment
|
||||
):
|
||||
self.browser_config.user_data_dir = str(xdg_paths.get_browser_profile_path(self._installation_mode))
|
||||
LOG.debug("No browser user_data_dir configured. Set browser.user_data_dir or --user-data-dir for non-test runs.")
|
||||
|
||||
# Chrome version detection and validation
|
||||
if has_remote_debugging:
|
||||
@@ -344,7 +339,7 @@ class WebScrapingMixin:
|
||||
user_data_dir_from_args,
|
||||
)
|
||||
if not effective_user_data_dir and not is_test_environment:
|
||||
effective_user_data_dir = str(xdg_paths.get_browser_profile_path(self._installation_mode))
|
||||
LOG.debug("No effective browser user_data_dir found. Browser will use its default profile location.")
|
||||
self.browser_config.user_data_dir = effective_user_data_dir
|
||||
|
||||
if not loggers.is_debug(LOG):
|
||||
@@ -365,6 +360,7 @@ class WebScrapingMixin:
|
||||
|
||||
# Enhanced profile directory handling
|
||||
if cfg.user_data_dir:
|
||||
xdg_paths.ensure_directory(Path(cfg.user_data_dir), "browser profile directory")
|
||||
profile_dir = os.path.join(cfg.user_data_dir, self.browser_config.profile_name or "Default")
|
||||
os.makedirs(profile_dir, exist_ok = True)
|
||||
prefs_file = os.path.join(profile_dir, "Preferences")
|
||||
|
||||
@@ -1,41 +1,61 @@
|
||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
|
||||
"""XDG Base Directory path resolution with backward compatibility.
|
||||
|
||||
Supports two installation modes:
|
||||
- Portable: All files in current working directory (for existing installations)
|
||||
- System-wide: Files organized in XDG directories (for new installations or package managers)
|
||||
"""
|
||||
"""XDG Base Directory path resolution with workspace abstraction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, replace
|
||||
from gettext import gettext as _
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal, cast
|
||||
from typing import Final, Literal
|
||||
|
||||
import platformdirs
|
||||
|
||||
from kleinanzeigen_bot.utils import loggers
|
||||
from kleinanzeigen_bot.utils.files import abspath
|
||||
|
||||
LOG:Final[loggers.Logger] = loggers.get_logger(__name__)
|
||||
|
||||
APP_NAME:Final[str] = "kleinanzeigen-bot"
|
||||
|
||||
InstallationMode = Literal["portable", "xdg"]
|
||||
PathCategory = Literal["config", "cache", "state"]
|
||||
|
||||
|
||||
def _normalize_mode(mode:str | InstallationMode) -> InstallationMode:
|
||||
"""Validate and normalize installation mode input."""
|
||||
if mode in {"portable", "xdg"}:
|
||||
return cast(InstallationMode, mode)
|
||||
raise ValueError(f"Unsupported installation mode: {mode}")
|
||||
@dataclass(frozen = True)
|
||||
class Workspace:
|
||||
"""Resolved workspace paths for all bot side effects."""
|
||||
|
||||
mode:InstallationMode
|
||||
config_file:Path
|
||||
config_dir:Path # root directory for mode-dependent artifacts
|
||||
log_file:Path | None
|
||||
state_dir:Path
|
||||
download_dir:Path
|
||||
browser_profile_dir:Path
|
||||
diagnostics_dir:Path
|
||||
|
||||
@classmethod
|
||||
def for_config(cls, config_file:Path, log_basename:str) -> Workspace:
|
||||
"""Build a portable-style workspace rooted at the config parent directory."""
|
||||
config_file = config_file.resolve()
|
||||
config_dir = config_file.parent
|
||||
state_dir = config_dir / ".temp"
|
||||
return cls(
|
||||
mode = "portable",
|
||||
config_file = config_file,
|
||||
config_dir = config_dir,
|
||||
log_file = config_dir / f"{log_basename}.log",
|
||||
state_dir = state_dir,
|
||||
download_dir = config_dir / "downloaded-ads",
|
||||
browser_profile_dir = state_dir / "browser-profile",
|
||||
diagnostics_dir = state_dir / "diagnostics",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_directory(path:Path, description:str) -> None:
|
||||
def ensure_directory(path:Path, description:str) -> None:
|
||||
"""Create directory and verify it exists."""
|
||||
LOG.debug("Creating directory: %s", path)
|
||||
try:
|
||||
@@ -47,15 +67,25 @@ def _ensure_directory(path:Path, description:str) -> None:
|
||||
raise NotADirectoryError(str(path))
|
||||
|
||||
|
||||
def _build_xdg_workspace(log_basename:str, config_file_override:Path | None = None) -> Workspace:
|
||||
"""Build an XDG-style workspace using standard user directories."""
|
||||
config_dir = get_xdg_base_dir("config").resolve()
|
||||
state_dir = get_xdg_base_dir("state").resolve()
|
||||
config_file = config_file_override.resolve() if config_file_override is not None else config_dir / "config.yaml"
|
||||
return Workspace(
|
||||
mode = "xdg",
|
||||
config_file = config_file,
|
||||
config_dir = config_dir,
|
||||
log_file = state_dir / f"{log_basename}.log",
|
||||
state_dir = state_dir,
|
||||
download_dir = config_dir / "downloaded-ads",
|
||||
browser_profile_dir = (get_xdg_base_dir("cache") / "browser-profile").resolve(),
|
||||
diagnostics_dir = (get_xdg_base_dir("cache") / "diagnostics").resolve(),
|
||||
)
|
||||
|
||||
|
||||
def get_xdg_base_dir(category:PathCategory) -> Path:
|
||||
"""Get XDG base directory for the given category.
|
||||
|
||||
Args:
|
||||
category: The XDG category (config, cache, or state)
|
||||
|
||||
Returns:
|
||||
Path to the XDG base directory for this app
|
||||
"""
|
||||
"""Get XDG base directory for the given category."""
|
||||
resolved:str | None = None
|
||||
match category:
|
||||
case "config":
|
||||
@@ -71,20 +101,12 @@ def get_xdg_base_dir(category:PathCategory) -> Path:
|
||||
raise RuntimeError(f"Failed to resolve XDG base directory for category: {category}")
|
||||
|
||||
base_dir = Path(resolved)
|
||||
|
||||
LOG.debug("XDG %s directory: %s", category, base_dir)
|
||||
return base_dir
|
||||
|
||||
|
||||
def detect_installation_mode() -> InstallationMode | None:
|
||||
"""Detect installation mode based on config file location.
|
||||
|
||||
Returns:
|
||||
"portable" if ./config.yaml exists in CWD
|
||||
"xdg" if config exists in XDG location
|
||||
None if neither exists (first run)
|
||||
"""
|
||||
# Check for portable installation (./config.yaml in CWD)
|
||||
def detect_installation_mode() -> Literal["portable", "xdg"] | None:
|
||||
"""Detect installation mode based on config file location."""
|
||||
portable_config = Path.cwd() / "config.yaml"
|
||||
LOG.debug("Checking for portable config at: %s", portable_config)
|
||||
|
||||
@@ -92,7 +114,6 @@ def detect_installation_mode() -> InstallationMode | None:
|
||||
LOG.debug("Detected installation mode: %s", "portable")
|
||||
return "portable"
|
||||
|
||||
# Check for XDG installation
|
||||
xdg_config = get_xdg_base_dir("config") / "config.yaml"
|
||||
LOG.debug("Checking for XDG config at: %s", xdg_config)
|
||||
|
||||
@@ -100,37 +121,37 @@ def detect_installation_mode() -> InstallationMode | None:
|
||||
LOG.debug("Detected installation mode: %s", "xdg")
|
||||
return "xdg"
|
||||
|
||||
# Neither exists - first run
|
||||
LOG.info("No existing configuration (portable or system-wide) found")
|
||||
return None
|
||||
|
||||
|
||||
def prompt_installation_mode() -> InstallationMode:
|
||||
"""Prompt user to choose installation mode on first run.
|
||||
|
||||
Returns:
|
||||
"portable" or "xdg" based on user choice, or "portable" as default for non-interactive mode
|
||||
"""
|
||||
# Check if running in non-interactive mode (no stdin or not a TTY)
|
||||
def prompt_installation_mode() -> Literal["portable", "xdg"]:
|
||||
"""Prompt user to choose installation mode on first run."""
|
||||
if not sys.stdin or not sys.stdin.isatty():
|
||||
LOG.info("Non-interactive mode detected, defaulting to portable installation")
|
||||
return "portable"
|
||||
|
||||
portable_ws = Workspace.for_config((Path.cwd() / "config.yaml").resolve(), APP_NAME)
|
||||
xdg_workspace = _build_xdg_workspace(APP_NAME)
|
||||
|
||||
print(_("Choose installation type:"))
|
||||
print(_("[1] Portable (current directory)"))
|
||||
print(_("[2] System-wide (XDG directories)"))
|
||||
print(f" config: {portable_ws.config_file}")
|
||||
print(f" log: {portable_ws.log_file}")
|
||||
print(_("[2] User directories (per-user standard locations)"))
|
||||
print(f" config: {xdg_workspace.config_file}")
|
||||
print(f" log: {xdg_workspace.log_file}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input(_("Enter 1 or 2: ")).strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
# Non-interactive or interrupted - default to portable
|
||||
print() # newline after ^C or EOF
|
||||
print()
|
||||
LOG.info("Defaulting to portable installation mode")
|
||||
return "portable"
|
||||
|
||||
if choice == "1":
|
||||
mode:InstallationMode = "portable"
|
||||
mode:Literal["portable", "xdg"] = "portable"
|
||||
LOG.info("User selected installation mode: %s", mode)
|
||||
return mode
|
||||
if choice == "2":
|
||||
@@ -140,130 +161,122 @@ def prompt_installation_mode() -> InstallationMode:
|
||||
print(_("Invalid choice. Please enter 1 or 2."))
|
||||
|
||||
|
||||
def get_config_file_path(mode:str | InstallationMode) -> Path:
|
||||
"""Get config.yaml file path for the given mode.
|
||||
|
||||
Args:
|
||||
mode: Installation mode (portable or xdg)
|
||||
|
||||
Returns:
|
||||
Path to config.yaml
|
||||
def _detect_mode_from_footprints_with_hits(
|
||||
config_file:Path,
|
||||
) -> tuple[Literal["portable", "xdg", "ambiguous", "unknown"], list[Path], list[Path]]:
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
config_path = Path.cwd() / "config.yaml" if mode == "portable" else get_xdg_base_dir("config") / "config.yaml"
|
||||
|
||||
LOG.debug("Resolving config file path for mode '%s': %s", mode, config_path)
|
||||
return config_path
|
||||
|
||||
|
||||
def get_ad_files_search_dir(mode:str | InstallationMode) -> Path:
|
||||
"""Get directory to search for ad files.
|
||||
|
||||
Ad files are searched relative to the config file directory,
|
||||
matching the documented behavior that glob patterns are relative to config.yaml.
|
||||
|
||||
Args:
|
||||
mode: Installation mode (portable or xdg)
|
||||
|
||||
Returns:
|
||||
Path to ad files search directory (same as config file directory)
|
||||
Detect workspace mode and return concrete footprint hits for diagnostics.
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
search_dir = Path.cwd() if mode == "portable" else get_xdg_base_dir("config")
|
||||
config_file = config_file.resolve()
|
||||
cwd_config = (Path.cwd() / "config.yaml").resolve()
|
||||
xdg_config_dir = get_xdg_base_dir("config").resolve()
|
||||
xdg_cache_dir = get_xdg_base_dir("cache").resolve()
|
||||
xdg_state_dir = get_xdg_base_dir("state").resolve()
|
||||
config_in_xdg_tree = config_file.is_relative_to(xdg_config_dir)
|
||||
|
||||
LOG.debug("Resolving ad files search directory for mode '%s': %s", mode, search_dir)
|
||||
return search_dir
|
||||
portable_hits:list[Path] = []
|
||||
xdg_hits:list[Path] = []
|
||||
|
||||
if config_file == cwd_config:
|
||||
portable_hits.append(cwd_config)
|
||||
if not config_in_xdg_tree:
|
||||
if (config_file.parent / ".temp").exists():
|
||||
portable_hits.append((config_file.parent / ".temp").resolve())
|
||||
if (config_file.parent / "downloaded-ads").exists():
|
||||
portable_hits.append((config_file.parent / "downloaded-ads").resolve())
|
||||
|
||||
if config_in_xdg_tree:
|
||||
xdg_hits.append(config_file)
|
||||
if not config_in_xdg_tree and (xdg_config_dir / "config.yaml").exists():
|
||||
xdg_hits.append((xdg_config_dir / "config.yaml").resolve())
|
||||
if (xdg_config_dir / "downloaded-ads").exists():
|
||||
xdg_hits.append((xdg_config_dir / "downloaded-ads").resolve())
|
||||
if (xdg_cache_dir / "browser-profile").exists():
|
||||
xdg_hits.append((xdg_cache_dir / "browser-profile").resolve())
|
||||
if (xdg_cache_dir / "diagnostics").exists():
|
||||
xdg_hits.append((xdg_cache_dir / "diagnostics").resolve())
|
||||
if (xdg_state_dir / "update_check_state.json").exists():
|
||||
xdg_hits.append((xdg_state_dir / "update_check_state.json").resolve())
|
||||
|
||||
portable_detected = len(portable_hits) > 0
|
||||
xdg_detected = len(xdg_hits) > 0
|
||||
|
||||
if portable_detected and xdg_detected:
|
||||
return "ambiguous", portable_hits, xdg_hits
|
||||
if portable_detected:
|
||||
return "portable", portable_hits, xdg_hits
|
||||
if xdg_detected:
|
||||
return "xdg", portable_hits, xdg_hits
|
||||
return "unknown", portable_hits, xdg_hits
|
||||
|
||||
|
||||
def get_downloaded_ads_path(mode:str | InstallationMode) -> Path:
|
||||
"""Get downloaded ads directory path.
|
||||
def _workspace_mode_resolution_error(
|
||||
config_file:Path,
|
||||
detected_mode:Literal["ambiguous", "unknown"],
|
||||
portable_hits:list[Path],
|
||||
xdg_hits:list[Path],
|
||||
) -> ValueError:
|
||||
def _format_hits(label:str, hits:list[Path]) -> str:
|
||||
if not hits:
|
||||
return f"{label}: {_('none')}"
|
||||
deduped = list(dict.fromkeys(hits))
|
||||
return f"{label}:\n- " + "\n- ".join(str(hit) for hit in deduped)
|
||||
|
||||
Args:
|
||||
mode: Installation mode (portable or xdg)
|
||||
|
||||
Returns:
|
||||
Path to downloaded ads directory
|
||||
|
||||
Note:
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
ads_path = Path.cwd() / "downloaded-ads" if mode == "portable" else get_xdg_base_dir("config") / "downloaded-ads"
|
||||
|
||||
LOG.debug("Resolving downloaded ads path for mode '%s': %s", mode, ads_path)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
_ensure_directory(ads_path, "downloaded ads directory")
|
||||
|
||||
return ads_path
|
||||
guidance = _(
|
||||
"Cannot determine workspace mode for --config=%(config_file)s. "
|
||||
"Use --workspace-mode=portable or --workspace-mode=xdg.\n"
|
||||
"For cleanup guidance, see: %(url)s"
|
||||
) % {
|
||||
"config_file": config_file,
|
||||
"url": "https://github.com/Second-Hand-Friends/kleinanzeigen-bot/blob/main/docs/CONFIGURATION.md#installation-modes",
|
||||
}
|
||||
details = f"{_format_hits(_('Portable footprint hits'), portable_hits)}\n{_format_hits(_('XDG footprint hits'), xdg_hits)}"
|
||||
if detected_mode == "ambiguous":
|
||||
return ValueError(f"{guidance}\n{_('Detected both portable and XDG footprints.')}\n{details}")
|
||||
return ValueError(f"{guidance}\n{_('Detected neither portable nor XDG footprints.')}\n{details}")
|
||||
|
||||
|
||||
def get_browser_profile_path(mode:str | InstallationMode, config_override:str | None = None) -> Path:
|
||||
"""Get browser profile directory path.
|
||||
def resolve_workspace(
|
||||
config_arg:str | None,
|
||||
logfile_arg:str | None,
|
||||
*,
|
||||
workspace_mode:InstallationMode | None,
|
||||
logfile_explicitly_provided:bool,
|
||||
log_basename:str,
|
||||
) -> Workspace:
|
||||
"""Resolve workspace paths from CLI flags and auto-detected installation mode."""
|
||||
config_path = Path(abspath(config_arg)).resolve() if config_arg else None
|
||||
mode = workspace_mode
|
||||
|
||||
Args:
|
||||
mode: Installation mode (portable or xdg)
|
||||
config_override: Optional config override path (takes precedence)
|
||||
if config_path and mode is None:
|
||||
detected_mode, portable_hits, xdg_hits = _detect_mode_from_footprints_with_hits(config_path)
|
||||
if detected_mode == "portable":
|
||||
mode = "portable"
|
||||
elif detected_mode == "xdg":
|
||||
mode = "xdg"
|
||||
else:
|
||||
raise _workspace_mode_resolution_error(
|
||||
config_path,
|
||||
detected_mode,
|
||||
portable_hits,
|
||||
xdg_hits,
|
||||
)
|
||||
|
||||
Returns:
|
||||
Path to browser profile directory
|
||||
if config_arg:
|
||||
if config_path is None or mode is None:
|
||||
raise RuntimeError("Workspace mode and config path must be resolved when --config is supplied")
|
||||
if mode == "portable":
|
||||
workspace = Workspace.for_config(config_path, log_basename)
|
||||
else:
|
||||
workspace = _build_xdg_workspace(log_basename, config_file_override = config_path)
|
||||
else:
|
||||
mode = mode or detect_installation_mode()
|
||||
if mode is None:
|
||||
mode = prompt_installation_mode()
|
||||
|
||||
Note:
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
if config_override:
|
||||
profile_path = Path(config_override).expanduser().resolve()
|
||||
LOG.debug("Resolving browser profile path for mode '%s' (config override): %s", mode, profile_path)
|
||||
elif mode == "portable":
|
||||
profile_path = (Path.cwd() / ".temp" / "browser-profile").resolve()
|
||||
LOG.debug("Resolving browser profile path for mode '%s': %s", mode, profile_path)
|
||||
else: # xdg
|
||||
profile_path = (get_xdg_base_dir("cache") / "browser-profile").resolve()
|
||||
LOG.debug("Resolving browser profile path for mode '%s': %s", mode, profile_path)
|
||||
workspace = Workspace.for_config((Path.cwd() / "config.yaml").resolve(), log_basename) if mode == "portable" else _build_xdg_workspace(log_basename)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
_ensure_directory(profile_path, "browser profile directory")
|
||||
if logfile_explicitly_provided:
|
||||
workspace = replace(workspace, log_file = Path(abspath(logfile_arg)).resolve() if logfile_arg else None)
|
||||
|
||||
return profile_path
|
||||
|
||||
|
||||
def get_log_file_path(basename:str, mode:str | InstallationMode) -> Path:
|
||||
"""Get log file path.
|
||||
|
||||
Args:
|
||||
basename: Log file basename (without .log extension)
|
||||
mode: Installation mode (portable or xdg)
|
||||
|
||||
Returns:
|
||||
Path to log file
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
log_path = Path.cwd() / f"{basename}.log" if mode == "portable" else get_xdg_base_dir("state") / f"{basename}.log"
|
||||
|
||||
LOG.debug("Resolving log file path for mode '%s': %s", mode, log_path)
|
||||
|
||||
# Create parent directory if it doesn't exist
|
||||
_ensure_directory(log_path.parent, "log directory")
|
||||
|
||||
return log_path
|
||||
|
||||
|
||||
def get_update_check_state_path(mode:str | InstallationMode) -> Path:
|
||||
"""Get update check state file path.
|
||||
|
||||
Args:
|
||||
mode: Installation mode (portable or xdg)
|
||||
|
||||
Returns:
|
||||
Path to update check state file
|
||||
"""
|
||||
mode = _normalize_mode(mode)
|
||||
state_path = Path.cwd() / ".temp" / "update_check_state.json" if mode == "portable" else get_xdg_base_dir("state") / "update_check_state.json"
|
||||
|
||||
LOG.debug("Resolving update check state path for mode '%s': %s", mode, state_path)
|
||||
|
||||
# Create parent directory if it doesn't exist
|
||||
_ensure_directory(state_path.parent, "update check state directory")
|
||||
|
||||
return state_path
|
||||
return workspace
|
||||
|
||||
Reference in New Issue
Block a user