mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-17 20:17:46 +02:00
Sync from development - prepare for v0.3.0
This commit is contained in:
296
jackify/backend/handlers/wabbajack_directory.py
Normal file
296
jackify/backend/handlers/wabbajack_directory.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user