mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:17:58 +02:00
297 lines
14 KiB
Python
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
|