From 30abb989280fd917ce7994f14bd605fb954ddd5a Mon Sep 17 00:00:00 2001 From: MohammadAmin Baranzehi Date: Tue, 1 Sep 2026 17:04:16 +0330 Subject: [PATCH] feat(core): rewrite installer with modular config, dynamic drive resolver, and clean orchestration --- main.py | 167 +++++++++++++++++++++++++++++++----- module/__init__.py | 17 +++- module/config.py | 64 ++++++++++++++ module/downloader_copier.py | 100 ++++++++++----------- module/extract_acc.py | 93 ++++++++++++-------- module/install_novindesk.py | 132 +++++++++++++++++----------- 6 files changed, 418 insertions(+), 155 deletions(-) create mode 100644 module/config.py diff --git a/main.py b/main.py index fcdf480..6bc7559 100644 --- a/main.py +++ b/main.py @@ -1,27 +1,148 @@ -import os +""" +Main Orchestrator for Acc Installer. +Executes directory setup, file copy, archive extraction, and automated installation. +""" + +import argparse +import logging import sys -import subprocess +import time +from typing import Optional -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -MODULE_DIR = os.path.join(BASE_DIR, "module") +from module.config import InstallerConfig, detect_target_drive +from module.downloader_copier import setup_directories_and_copy +from module.extract_acc import extract_acc_archive +from module.install_novindesk import install_novindesk -scripts = [ - "downloader_copier.py", - "extract_acc.py", - "install_novindesk.py" -] -for script in scripts: - script_path = os.path.join(MODULE_DIR, script) - print(f"[+] Running {script}...") - result = subprocess.run([sys.executable, script_path], capture_output=True, text=True) - - if result.returncode == 0: - print(f"[+] {script} executed successfully.\n") - else: - print(f"[-] Error running {script}:") - if result.stdout: - print(result.stdout) - if result.stderr: - print(result.stderr) - break +def setup_logger(verbose: bool = False) -> logging.Logger: + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S" + ) + return logging.getLogger("acc_installer") + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="NovinPardaz Acc Installer - Automated Setup Pipeline", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "--source", + type=str, + default=r"C:\SoftwareNP", + help="Source directory containing installation archives" + ) + parser.add_argument( + "--drive", + type=str, + default=None, + help="Target drive for installation (e.g. 'D:\\'). Auto-detected if not specified." + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Simulate execution without modifying files or launching processes" + ) + parser.add_argument( + "--skip-acc", + action="store_true", + help="Skip extraction of NovinAcc archive" + ) + parser.add_argument( + "--skip-desk", + action="store_true", + help="Skip installation of NovinDesk" + ) + parser.add_argument( + "--no-ui", + action="store_true", + help="Disable automatic UI interaction during NovinDesk installation" + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable detailed debug logging" + ) + return parser.parse_args() + + +def run_pipeline(config: InstallerConfig, + skip_acc: bool = False, + skip_desk: bool = False, + auto_ui: bool = True) -> bool: + """ + Executes the installer pipeline steps sequentially. + """ + start_time = time.time() + print("=" * 60) + print(" NovinPardaz Acc Installer Pipeline ") + print("=" * 60) + print(f" Source Directory : {config.source_dir}") + print(f" Target Drive : {config.target_drive}") + print(f" Target Base Dir : {config.base_dir}") + print(f" Dry Run Mode : {config.dry_run}") + print("-" * 60) + + try: + # Step 1: Directory Setup & Software Copy + print("\n[Step 1/3] Setting up directories and copying software files...") + setup_directories_and_copy(config) + print("[+] Step 1 completed successfully.") + + # Step 2: Extract NovinAcc Archive + if not skip_acc: + print("\n[Step 2/3] Extracting NovinAcc accounting archive...") + extract_acc_archive(config) + print("[+] Step 2 completed successfully.") + else: + print("\n[Step 2/3] Skipped NovinAcc extraction (--skip-acc specified).") + + # Step 3: Install NovinDesk + if not skip_desk: + print("\n[Step 3/3] Extracting and launching NovinDesk installer...") + install_novindesk(config, auto_ui=auto_ui) + print("[+] Step 3 completed successfully.") + else: + print("\n[Step 3/3] Skipped NovinDesk installation (--skip-desk specified).") + + elapsed = time.time() - start_time + print("\n" + "=" * 60) + print(f"[SUCCESS] All installation steps completed in {elapsed:.2f} seconds.") + print("=" * 60) + return True + + except Exception as exc: + elapsed = time.time() - start_time + print(f"\n[-] Installation pipeline failed after {elapsed:.2f} seconds!") + print(f"[-] Error: {exc}", file=sys.stderr) + return False + + +def main() -> None: + args = parse_arguments() + setup_logger(args.verbose) + + target_drive = args.drive if args.drive else detect_target_drive() + config = InstallerConfig( + source_dir=args.source, + target_drive=target_drive, + dry_run=args.dry_run + ) + + success = run_pipeline( + config=config, + skip_acc=args.skip_acc, + skip_desk=args.skip_desk, + auto_ui=not args.no_ui + ) + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/module/__init__.py b/module/__init__.py index 46e58ea..cfcd2dc 100644 --- a/module/__init__.py +++ b/module/__init__.py @@ -1 +1,16 @@ -# Module package +""" +NovinPardaz Acc Installer Package. +""" + +from module.config import InstallerConfig, detect_target_drive +from module.downloader_copier import setup_directories_and_copy +from module.extract_acc import extract_acc_archive +from module.install_novindesk import install_novindesk + +__all__ = [ + "InstallerConfig", + "detect_target_drive", + "setup_directories_and_copy", + "extract_acc_archive", + "install_novindesk", +] diff --git a/module/config.py b/module/config.py new file mode 100644 index 0000000..29a31f8 --- /dev/null +++ b/module/config.py @@ -0,0 +1,64 @@ +""" +Configuration and environment resolution for Acc Installer. +""" + +from dataclasses import dataclass, field +import logging +import os +from pathlib import Path +from typing import List, Optional + +logger = logging.getLogger("acc_installer") + + +def detect_target_drive(candidates: Optional[List[str]] = None) -> str: + """ + Find the first available non-system drive on Windows. + Falls back to 'C:\\' if no secondary drive is found. + """ + if candidates is None: + candidates = ["D", "E", "F", "G", "H"] + + for letter in candidates: + drive_path = f"{letter}:\\" + if os.path.exists(drive_path): + logger.info("Found candidate drive: %s", drive_path) + return drive_path + + logger.warning("No secondary drive found in candidates. Falling back to C:\\") + return "C:\\" + + +@dataclass +class InstallerConfig: + """ + Centralized configuration holding paths and operational flags for the installer. + """ + source_dir: Path = field(default_factory=lambda: Path(r"C:\SoftwareNP")) + target_drive: str = field(default_factory=detect_target_drive) + dry_run: bool = False + + # Calculated paths + base_dir: Path = field(init=False) + soft_dir: Path = field(init=False) + acc_dir: Path = field(init=False) + + def __post_init__(self): + # Normalize target drive + if not self.target_drive.endswith("\\") and not self.target_drive.endswith("/"): + self.target_drive = f"{self.target_drive}\\" + + drive_path = Path(self.target_drive) + self.base_dir = drive_path / "NovinPardazOne" + self.soft_dir = self.base_dir / "NovinSoft" + self.acc_dir = self.base_dir / "NovinAcc" + + def ensure_directories(self) -> None: + """Create necessary target directories if they do not exist.""" + if self.dry_run: + logger.info("[DRY-RUN] Would create: %s and %s", self.soft_dir, self.acc_dir) + return + + self.soft_dir.mkdir(parents=True, exist_ok=True) + self.acc_dir.mkdir(parents=True, exist_ok=True) + logger.debug("Ensured directories at %s and %s", self.soft_dir, self.acc_dir) diff --git a/module/downloader_copier.py b/module/downloader_copier.py index 4cf790f..98d3c2c 100644 --- a/module/downloader_copier.py +++ b/module/downloader_copier.py @@ -1,63 +1,65 @@ +""" +Directory setup and software file copying module. +""" + +import logging import os +from pathlib import Path import shutil +from typing import Optional -def get_non_c_drive() -> str: +from module.config import InstallerConfig, detect_target_drive + +logger = logging.getLogger("acc_installer") + + +def setup_directories_and_copy(config: Optional[InstallerConfig] = None) -> Path: """ - Find the first available non-C drive on the target system. + Create directories on the target drive and copy software packages from source. """ - possible_drives = [f"{letter}:\\" for letter in "DEFGH"] - for drive in possible_drives: - if os.path.exists(drive): - return drive - return r"C:\\" + if config is None: + config = InstallerConfig() -def setup_directories_and_copy(source_dir: str = r"C:\SoftwareNP", - target_drive: str = None) -> str: - """ - Create directories on a non-C drive and copy files from source to NovinSoft. - """ - if not target_drive: - base_drive = get_non_c_drive() - else: - base_drive = target_drive + print(f"[+] Selected target drive: {config.target_drive}") + print(f"[+] Target base directory: {config.base_dir}") - # Base target directory set to NovinPardazOne - base_target_dir = os.path.join(base_drive, "NovinPardazOne") - acc_dir = os.path.join(base_target_dir, "NovinAcc") - soft_dir = os.path.join(base_target_dir, "NovinSoft") + # Ensure source directory exists + source_dir = Path(config.source_dir) + if not source_dir.exists(): + raise FileNotFoundError(f"Source directory does not exist: {source_dir}") - print(f"[+] Selected target drive: {base_drive}") - print(f"[+] Creating directory structure at {base_target_dir} ...") + # Create destination directories + config.ensure_directories() + print(f" [+] Target directories ready at {config.soft_dir}") - # Create NovinAcc and NovinSoft directories - os.makedirs(acc_dir, exist_ok=True) - os.makedirs(soft_dir, exist_ok=True) - print(" [+] NovinAcc and NovinSoft directories created successfully.") + print(f"[+] Copying files from {source_dir} to {config.soft_dir} ...") + + if config.dry_run: + items = list(source_dir.iterdir()) + print(f" [DRY-RUN] Would copy {len(items)} items from {source_dir} to {config.soft_dir}") + return config.soft_dir + + copied_count = 0 + for item in source_dir.iterdir(): + dest = config.soft_dir / item.name + if item.is_dir(): + shutil.copytree(item, dest, dirs_exist_ok=True) + else: + shutil.copy2(item, dest) + copied_count += 1 + + print(f" [+] {copied_count} file(s)/folder(s) copied successfully to {config.soft_dir}.") + return config.soft_dir - # Copy contents of source directory to NovinSoft - if os.path.exists(source_dir): - print(f"[+] Copying files from {source_dir} to {soft_dir} ...") - - for item in os.listdir(source_dir): - s = os.path.join(source_dir, item) - d = os.path.join(soft_dir, item) - - if os.path.isdir(s): - shutil.copytree(s, d, dirs_exist_ok=True) - else: - shutil.copy2(s, d) - - print(f" [+] All files copied successfully to {soft_dir}.") - return soft_dir - else: - raise FileNotFoundError(f"[-] Source directory not found at {source_dir}!") if __name__ == "__main__": - os.system("chcp 65001 > nul") - print("[+] Testing directory creation and software copy module ...") - + import sys + logging.basicConfig(level=logging.INFO, format="%(message)s") + print("[*] Running directory setup and copier module standalone ...") try: - target_path = setup_directories_and_copy(target_drive=r"D:\\") - print(f"\n[+] Operation completed successfully. Software path: {target_path}") + cfg = InstallerConfig(target_drive=detect_target_drive()) + setup_directories_and_copy(cfg) + print("[+] Directory copier module completed successfully.") except Exception as e: - print(f"\n[-] Error: {e}") + print(f"[-] Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/module/extract_acc.py b/module/extract_acc.py index 7b027cd..43799a2 100644 --- a/module/extract_acc.py +++ b/module/extract_acc.py @@ -1,50 +1,75 @@ +""" +Archive extractor module for NovinAcc accounting software. +""" + +import logging import os +from pathlib import Path import shutil +from typing import Optional import zipfile -def extract_acc_archive(base_drive: str = r"D:\\") -> str: - # Base directory set to NovinPardazOne - base_dir = os.path.join(base_drive, "NovinPardazOne") - soft_dir = os.path.join(base_dir, "NovinSoft") - acc_dir = os.path.join(base_dir, "NovinAcc") +from module.config import InstallerConfig, detect_target_drive - target_zip_name = "NovinAcc.zip" - source_zip_path = os.path.join(soft_dir, target_zip_name) - target_zip_path = os.path.join(acc_dir, target_zip_name) +logger = logging.getLogger("acc_installer") - # 1. Check if NovinSoft directory exists - if not os.path.exists(soft_dir): - raise FileNotFoundError(f"[-] Directory not found: {soft_dir}") - # 2. Check if zip file exists - if not os.path.exists(source_zip_path): - raise FileNotFoundError(f"[-] File {target_zip_name} was not found in {soft_dir}!") +def extract_acc_archive(config: Optional[InstallerConfig] = None, + archive_name: str = "NovinAcc.zip") -> Path: + """ + Locates NovinAcc archive in NovinSoft, moves it to NovinAcc, and extracts its contents. + """ + if config is None: + config = InstallerConfig() - # 3. Create NovinAcc directory if it doesn't exist - os.makedirs(acc_dir, exist_ok=True) + source_zip = config.soft_dir / archive_name + target_zip = config.acc_dir / archive_name - # 4. Move zip file to NovinAcc - print(f"[+] Moving {target_zip_name} to NovinAcc directory...") - shutil.move(source_zip_path, target_zip_path) - print(" [+] Zip file moved successfully.") + config.ensure_directories() - # 5. Extract archive - print(f"[+] Extracting archive to {acc_dir}...") - with zipfile.ZipFile(target_zip_path, 'r') as zip_ref: - zip_ref.extractall(acc_dir) - print(" [+] All files extracted successfully.") + if config.dry_run: + print(f" [DRY-RUN] Would locate {archive_name} in {config.soft_dir}, move to {config.acc_dir} and extract.") + return config.acc_dir - # 6. Remove temporary zip file - if os.path.exists(target_zip_path): - os.remove(target_zip_path) - print(" [+] Temporary zip file deleted.") + # Check if archive exists in soft_dir or already in acc_dir + archive_to_extract = None + if source_zip.exists(): + archive_to_extract = source_zip + elif target_zip.exists(): + archive_to_extract = target_zip + else: + raise FileNotFoundError( + f"Archive '{archive_name}' was not found in either {config.soft_dir} or {config.acc_dir}!" + ) + + # Move archive to target NovinAcc directory if not already there + if archive_to_extract != target_zip: + print(f"[+] Moving {archive_name} to NovinAcc directory...") + shutil.move(str(source_zip), str(target_zip)) + print(" [+] Zip archive moved successfully.") + + # Extract archive + print(f"[+] Extracting {archive_name} into {config.acc_dir} ...") + with zipfile.ZipFile(target_zip, 'r') as zip_ref: + zip_ref.extractall(config.acc_dir) + print(" [+] All archive files extracted successfully.") + + # Remove temporary zip file + if target_zip.exists(): + target_zip.unlink() + print(" [+] Temporary zip archive cleaned up.") + + return config.acc_dir - return acc_dir if __name__ == "__main__": - os.system("chcp 65001 > nul") - print("[+] Running move and extract module ...") + import sys + logging.basicConfig(level=logging.INFO, format="%(message)s") + print("[*] Running NovinAcc archive extractor standalone ...") try: - extract_acc_archive(base_drive=r"D:\\") + cfg = InstallerConfig(target_drive=detect_target_drive()) + extract_acc_archive(cfg) + print("[+] NovinAcc extraction completed successfully.") except Exception as e: - print(f"[-] Error: {e}") + print(f"[-] Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/module/install_novindesk.py b/module/install_novindesk.py index c1b3485..9b1093e 100644 --- a/module/install_novindesk.py +++ b/module/install_novindesk.py @@ -1,71 +1,107 @@ +""" +Extractor and automated installer runner for NovinDesk. +""" + +import logging import os -import zipfile +from pathlib import Path import subprocess import time +from typing import Optional +import zipfile -def install_novindesk(base_drive: str = r"D:\\") -> str: +from module.config import InstallerConfig, detect_target_drive + +logger = logging.getLogger("acc_installer") + + +def automate_installer_dialog() -> bool: """ - Extract and automatically install NovinDesk by navigating the installer UI. + Automate UI dialog clicks for silent installation using pyautogui. """ - base_dir = os.path.join(base_drive, "NovinPardazOne") - soft_dir = os.path.join(base_dir, "NovinSoft") - - zip_filename = "NovinDesk.zip" - source_zip_path = os.path.join(soft_dir, zip_filename) - extract_target_dir = os.path.join(soft_dir, "NovinDesk") - - if not os.path.exists(source_zip_path): - raise FileNotFoundError(f"[-] File {zip_filename} was not found in {soft_dir}!") - - os.makedirs(extract_target_dir, exist_ok=True) - - print(f"[+] Extracting {zip_filename} ...") - with zipfile.ZipFile(source_zip_path, 'r') as zip_ref: - zip_ref.extractall(extract_target_dir) - print(" [+] Extraction completed successfully.") - - exe_file = None - for root, dirs, files in os.walk(extract_target_dir): - for file in files: - if file.lower().endswith(".exe"): - exe_file = os.path.join(root, file) - break - - if not exe_file: - raise FileNotFoundError("[-] No executable file (.exe) found!") - - print(f"[+] Launching installer {os.path.basename(exe_file)} ...") - - # Launch application for installation - subprocess.Popen([exe_file, "--install"]) - - # Wait 5 seconds for the window to fully load - time.sleep(5) - try: - import pyautogui - + import pyautogui # type: ignore + # 1. Close any secondary/side dialogs pyautogui.press('escape') time.sleep(0.5) - # 2. Press Tab 4 times to navigate directly to the 'Accept & Install' button + # 2. Press Tab 4 times to navigate directly to 'Accept & Install' for _ in range(4): pyautogui.press('tab') time.sleep(0.2) - + # 3. Press Enter on the install button pyautogui.press('enter') - print(" [+] Confirmation sent and automated installation started.") + print(" [+] Sent automated UI confirmation successfully.") + return True + except ImportError: + print(" [!] Warning: 'pyautogui' is not installed. Skipping UI automation.") + return False except Exception as err: - print(f" [-] Automation error: {err}") + print(f" [-] UI Automation error: {err}") + return False + + +def install_novindesk(config: Optional[InstallerConfig] = None, + archive_name: str = "NovinDesk.zip", + auto_ui: bool = True) -> Path: + """ + Extracts NovinDesk archive, finds the installer executable, and runs installation. + """ + if config is None: + config = InstallerConfig() + + source_zip = config.soft_dir / archive_name + extract_target_dir = config.soft_dir / "NovinDesk" + + if config.dry_run: + print(f" [DRY-RUN] Would locate {archive_name}, extract into {extract_target_dir}, and launch installer.") + return extract_target_dir + + if not source_zip.exists() and not extract_target_dir.exists(): + raise FileNotFoundError(f"Neither '{source_zip}' nor '{extract_target_dir}' exists!") + + # Extract archive if needed + if source_zip.exists(): + extract_target_dir.mkdir(parents=True, exist_ok=True) + print(f"[+] Extracting {archive_name} into {extract_target_dir} ...") + with zipfile.ZipFile(source_zip, 'r') as zip_ref: + zip_ref.extractall(extract_target_dir) + print(" [+] Extraction completed successfully.") + + # Locate executable + exe_file = None + for root, _, files in os.walk(extract_target_dir): + for file in files: + if file.lower().endswith(".exe"): + exe_file = Path(root) / file + break + if exe_file: + break + + if not exe_file: + raise FileNotFoundError(f"No executable (.exe) found in {extract_target_dir}!") + + print(f"[+] Launching installer {exe_file.name} ...") + subprocess.Popen([str(exe_file), "--install"]) + + if auto_ui: + print(" [*] Waiting 5 seconds for installer window to initialize...") + time.sleep(5) + automate_installer_dialog() return exe_file + if __name__ == "__main__": - os.system("chcp 65001 > nul") - print("[+] Running NovinDesk installation module ...") + import sys + logging.basicConfig(level=logging.INFO, format="%(message)s") + print("[*] Running NovinDesk installation module standalone ...") try: - install_novindesk(base_drive=r"D:\\") + cfg = InstallerConfig(target_drive=detect_target_drive()) + install_novindesk(cfg) + print("[+] NovinDesk installation module completed successfully.") except Exception as e: - print(f"[-] Error: {e}") + print(f"[-] Error: {e}", file=sys.stderr) + sys.exit(1)