Files
Jackify/jackify/backend/handlers/wabbajack_directory.py
2026-02-07 18:26:54 +00:00

297 lines
14 KiB
Python

"""Directory and download methods for InstallWabbajackHandler (Mixin)."""
import logging
import os
import shutil
from pathlib import Path
from typing import Optional
import requests
from .ui_colors import COLOR_ERROR, COLOR_INFO, COLOR_PROMPT, COLOR_RESET, COLOR_WARNING
logger = logging.getLogger(__name__)
DEFAULT_WABBAJACK_PATH = "~/Wabbajack"
DEFAULT_WABBAJACK_NAME = "Wabbajack"
READLINE_AVAILABLE = False
try:
import readline
READLINE_AVAILABLE = True
except ImportError:
pass
except Exception as e:
logging.warning(f"Readline import failed: {e}")
try:
from .menu_handler import simple_path_completer
except ImportError:
simple_path_completer = None
class WabbajackDirectoryMixin:
"""Mixin providing directory setup and download methods."""
def _download_file(self, url: str, destination_path: Path) -> bool:
"""Downloads a file from a URL to a destination path.
Handles temporary file and overwrites destination if download succeeds.
Args:
url (str): The URL to download from.
destination_path (Path): The path to save the downloaded file.
Returns:
bool: True if download succeeds, False otherwise.
"""
self.logger.info(f"Downloading {destination_path.name} from {url}")
destination_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = destination_path.with_suffix(destination_path.suffix + ".part")
self.logger.debug(f"Downloading to temporary path: {temp_path}")
try:
with requests.get(url, stream=True, timeout=30, verify=True) as r:
r.raise_for_status()
block_size = 8192
with open(temp_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=block_size):
if chunk:
f.write(chunk)
actual_downloaded_size = temp_path.stat().st_size
self.logger.debug(f"Download finished. Actual size: {actual_downloaded_size} bytes.")
shutil.move(str(temp_path), str(destination_path))
self.logger.info(f"Successfully downloaded and moved to {destination_path}")
return True
except requests.exceptions.RequestException as e:
self.logger.error(f"Download failed for {url}: {e}", exc_info=True)
print(f"\n{COLOR_ERROR}Error downloading {destination_path.name}: {e}{COLOR_RESET}")
if temp_path.exists():
try:
temp_path.unlink()
except OSError as unlink_err:
self.logger.error(f"Failed to remove partial download {temp_path}: {unlink_err}")
return False
except Exception as e:
self.logger.error(f"An unexpected error occurred during download: {e}", exc_info=True)
print(f"\n{COLOR_ERROR}An unexpected error occurred during download: {e}{COLOR_RESET}")
if temp_path.exists():
try:
temp_path.unlink()
except OSError as unlink_err:
self.logger.error(f"Failed to remove partial download {temp_path}: {unlink_err}")
return False
def _prepare_install_directory(self) -> bool:
"""
Ensures the target installation directory exists and is accessible.
Handles directory creation, prompting the user if outside $HOME.
Returns:
bool: True if the directory exists and is ready, False otherwise.
"""
if not self.install_path:
self.logger.error("Cannot prepare directory: install_path is not set.")
return False
self.logger.info(f"Preparing installation directory: {self.install_path}")
if self.install_path.exists():
if self.install_path.is_dir():
self.logger.info(f"Directory already exists: {self.install_path}")
if not os.access(self.install_path, os.W_OK | os.X_OK):
print(f"{COLOR_ERROR}Error: Directory exists but lacks necessary write/execute permissions.{COLOR_RESET}")
return False
return True
else:
print(f"{COLOR_ERROR}Error: The specified path exists but is a file, not a directory.{COLOR_RESET}")
return False
else:
self.logger.info("Directory does not exist. Attempting creation...")
try:
home_dir = Path.home()
is_outside_home = not str(self.install_path.resolve()).startswith(str(home_dir.resolve()))
if is_outside_home:
self.logger.warning(f"Install path {self.install_path} is outside home directory {home_dir}.")
print(f"\n{COLOR_PROMPT}The chosen path is outside your home directory and may require manual creation.{COLOR_RESET}")
while True:
response = input(f"{COLOR_PROMPT}Please create the directory \"{self.install_path}\" manually,\nensure you have write permissions, and then press Enter to continue (or 'q' to quit): {COLOR_RESET}").lower()
if response == 'q':
self.logger.warning("User aborted manual directory creation.")
return False
if self.install_path.exists():
if self.install_path.is_dir():
self.logger.info("Directory created manually by user.")
if not os.access(self.install_path, os.W_OK | os.X_OK):
print(f"{COLOR_WARNING}Warning: Directory created, but write/execute permissions might be missing.{COLOR_RESET}")
return True
else:
print(f"{COLOR_ERROR}Error: Path exists now, but it is not a directory. Please fix and try again.{COLOR_RESET}")
else:
print(f"\n{COLOR_ERROR}Directory still not found. Please create it or enter 'q' to quit.{COLOR_RESET}")
else:
self.logger.info("Path is inside home directory. Creating...")
os.makedirs(self.install_path)
self.logger.info(f"Successfully created directory: {self.install_path}")
if not os.access(self.install_path, os.W_OK | os.X_OK):
print(f"{COLOR_WARNING}Warning: Directory created, but lacks write/execute permissions. Subsequent steps might fail.{COLOR_RESET}")
return True
except PermissionError:
self.logger.error(f"Permission denied when trying to create directory: {self.install_path}", exc_info=True)
print(f"\n{COLOR_ERROR}Error: Permission denied creating directory.{COLOR_RESET}")
print(f"{COLOR_INFO}Please check permissions for the parent directory or choose a different location.{COLOR_RESET}")
return False
except OSError as e:
self.logger.error(f"Failed to create directory {self.install_path}: {e}", exc_info=True)
print(f"\n{COLOR_ERROR}Error creating directory: {e}{COLOR_RESET}")
return False
except Exception as e:
self.logger.error(f"An unexpected error occurred during directory preparation: {e}", exc_info=True)
print(f"\n{COLOR_ERROR}An unexpected error occurred: {e}{COLOR_RESET}")
return False
def _get_wabbajack_install_path(self) -> Optional[Path]:
"""
Prompts the user for the Wabbajack installation path with tab completion.
Uses the FileSystemHandler for path validation and completion.
Returns:
Optional[Path]: The chosen installation path as a Path object, or None if cancelled.
"""
self.logger.info("Prompting for Wabbajack installation path.")
current_path = self.install_path if self.install_path else Path(DEFAULT_WABBAJACK_PATH).expanduser()
if READLINE_AVAILABLE and simple_path_completer:
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(simple_path_completer)
try:
while True:
try:
prompt_text = f"{COLOR_PROMPT}Enter Wabbajack installation path (default: {current_path}): {COLOR_RESET}"
user_input = input(prompt_text).strip()
if not user_input:
chosen_path_str = str(current_path)
else:
chosen_path_str = user_input
chosen_path = Path(chosen_path_str).expanduser().resolve()
if not chosen_path.name:
print(f"{COLOR_ERROR}Invalid path. Please enter a valid directory path.{COLOR_RESET}")
continue
if chosen_path.exists() and not chosen_path.is_dir():
print(f"{COLOR_ERROR}Path exists but is not a directory: {chosen_path}{COLOR_RESET}")
continue
confirm_prompt = f"{COLOR_PROMPT}Install Wabbajack to {chosen_path}? (Y/n/c to cancel): {COLOR_RESET}"
confirmation = input(confirm_prompt).lower()
if confirmation == 'c':
self.logger.info("Wabbajack installation path selection cancelled by user.")
return None
elif confirmation != 'n':
self.install_path = chosen_path
self.logger.info(f"Wabbajack installation path set to: {self.install_path}")
return self.install_path
except KeyboardInterrupt:
self.logger.info("Wabbajack installation path selection cancelled by user (Ctrl+C).")
print("\nPath selection cancelled.")
return None
except Exception as e:
self.logger.error(f"Error during path input: {e}", exc_info=True)
print(f"{COLOR_ERROR}An unexpected error occurred: {e}{COLOR_RESET}")
return None
finally:
if READLINE_AVAILABLE:
readline.set_completer(None)
def _get_wabbajack_shortcut_name(self) -> Optional[str]:
"""
Prompts the user for the Wabbajack shortcut name.
Returns:
Optional[str]: The name chosen by the user, or None if cancelled.
"""
self.logger.debug("Getting Wabbajack shortcut name.")
if self.shortcut_name:
self.logger.info(f"Using pre-configured shortcut name: {self.shortcut_name}")
return self.shortcut_name
chosen_name = DEFAULT_WABBAJACK_NAME
if self.menu_handler:
self.logger.debug("Using menu_handler for shortcut name input")
print(f"\nWabbajack Shortcut Name:")
name_input = self.menu_handler.get_input_with_default(
prompt=f"Enter the desired name for the Wabbajack Steam shortcut (default: {chosen_name})",
default=chosen_name
)
if name_input is not None:
self.logger.info(f"User provided shortcut name: {name_input}")
return name_input
else:
self.logger.info("User cancelled shortcut name input")
return None
try:
print(f"\n{COLOR_PROMPT}Enter the desired name for the Wabbajack Steam shortcut.{COLOR_RESET}")
name_input = input(f"{COLOR_PROMPT}Name [{chosen_name}]: {COLOR_RESET}").strip()
if not name_input:
self.logger.info(f"User did not provide input, using default name: {chosen_name}")
else:
chosen_name = name_input
self.logger.info(f"User provided name: {chosen_name}")
return chosen_name
except KeyboardInterrupt:
print(f"\n{COLOR_ERROR}Input cancelled by user.{COLOR_RESET}")
self.logger.warning("User cancelled name input.")
return None
except Exception as e:
self.logger.error(f"An unexpected error occurred while getting name input: {e}", exc_info=True)
return None
def _download_wabbajack_executable(self) -> bool:
"""
Downloads the latest Wabbajack.exe to the install directory.
Checks existence first.
Returns:
bool: True on success or if file exists, False otherwise.
"""
if not self.install_path:
self.logger.error("Cannot download Wabbajack.exe: install_path is not set.")
return False
url = "https://github.com/wabbajack-tools/wabbajack/releases/latest/download/Wabbajack.exe"
destination = self.install_path / "Wabbajack.exe"
if destination.is_file():
self.logger.info(f"Wabbajack.exe already exists at {destination}. Skipping download.")
return True
self.logger.info("Wabbajack.exe not found. Downloading...")
if self._download_file(url, destination):
try:
os.chmod(destination, 0o755)
self.logger.info(f"Set execute permissions on {destination}")
except Exception as e:
self.logger.warning(f"Could not set execute permission on {destination}: {e}")
self.logger.warning("Could not set execute permission on Wabbajack.exe.")
return True
else:
self.logger.error("Failed to download Wabbajack.exe.")
return False