feat(core): rewrite installer with modular config, dynamic drive resolver, and clean orchestration

This commit is contained in:
2026-09-01 17:04:16 +03:30
parent ce34e66154
commit 30abb98928
6 changed files with 418 additions and 155 deletions

167
main.py
View File

@@ -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 sys
import subprocess import time
from typing import Optional
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) from module.config import InstallerConfig, detect_target_drive
MODULE_DIR = os.path.join(BASE_DIR, "module") 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: def setup_logger(verbose: bool = False) -> logging.Logger:
script_path = os.path.join(MODULE_DIR, script) level = logging.DEBUG if verbose else logging.INFO
print(f"[+] Running {script}...") logging.basicConfig(
result = subprocess.run([sys.executable, script_path], capture_output=True, text=True) level=level,
format="%(asctime)s [%(levelname)s] %(message)s",
if result.returncode == 0: datefmt="%H:%M:%S"
print(f"[+] {script} executed successfully.\n") )
else: return logging.getLogger("acc_installer")
print(f"[-] Error running {script}:")
if result.stdout:
print(result.stdout) def parse_arguments() -> argparse.Namespace:
if result.stderr: parser = argparse.ArgumentParser(
print(result.stderr) description="NovinPardaz Acc Installer - Automated Setup Pipeline",
break 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()

View File

@@ -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",
]

64
module/config.py Normal file
View File

@@ -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)

View File

@@ -1,63 +1,65 @@
"""
Directory setup and software file copying module.
"""
import logging
import os import os
from pathlib import Path
import shutil 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"] if config is None:
for drive in possible_drives: config = InstallerConfig()
if os.path.exists(drive):
return drive
return r"C:\\"
def setup_directories_and_copy(source_dir: str = r"C:\SoftwareNP", print(f"[+] Selected target drive: {config.target_drive}")
target_drive: str = None) -> str: print(f"[+] Target base directory: {config.base_dir}")
"""
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
# Base target directory set to NovinPardazOne # Ensure source directory exists
base_target_dir = os.path.join(base_drive, "NovinPardazOne") source_dir = Path(config.source_dir)
acc_dir = os.path.join(base_target_dir, "NovinAcc") if not source_dir.exists():
soft_dir = os.path.join(base_target_dir, "NovinSoft") raise FileNotFoundError(f"Source directory does not exist: {source_dir}")
print(f"[+] Selected target drive: {base_drive}") # Create destination directories
print(f"[+] Creating directory structure at {base_target_dir} ...") config.ensure_directories()
print(f" [+] Target directories ready at {config.soft_dir}")
# Create NovinAcc and NovinSoft directories print(f"[+] Copying files from {source_dir} to {config.soft_dir} ...")
os.makedirs(acc_dir, exist_ok=True)
os.makedirs(soft_dir, exist_ok=True) if config.dry_run:
print(" [+] NovinAcc and NovinSoft directories created successfully.") 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__": if __name__ == "__main__":
os.system("chcp 65001 > nul") import sys
print("[+] Testing directory creation and software copy module ...") logging.basicConfig(level=logging.INFO, format="%(message)s")
print("[*] Running directory setup and copier module standalone ...")
try: try:
target_path = setup_directories_and_copy(target_drive=r"D:\\") cfg = InstallerConfig(target_drive=detect_target_drive())
print(f"\n[+] Operation completed successfully. Software path: {target_path}") setup_directories_and_copy(cfg)
print("[+] Directory copier module completed successfully.")
except Exception as e: except Exception as e:
print(f"\n[-] Error: {e}") print(f"[-] Error: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -1,50 +1,75 @@
"""
Archive extractor module for NovinAcc accounting software.
"""
import logging
import os import os
from pathlib import Path
import shutil import shutil
from typing import Optional
import zipfile import zipfile
def extract_acc_archive(base_drive: str = r"D:\\") -> str: from module.config import InstallerConfig, detect_target_drive
# 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")
target_zip_name = "NovinAcc.zip" logger = logging.getLogger("acc_installer")
source_zip_path = os.path.join(soft_dir, target_zip_name)
target_zip_path = os.path.join(acc_dir, target_zip_name)
# 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 def extract_acc_archive(config: Optional[InstallerConfig] = None,
if not os.path.exists(source_zip_path): archive_name: str = "NovinAcc.zip") -> Path:
raise FileNotFoundError(f"[-] File {target_zip_name} was not found in {soft_dir}!") """
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 source_zip = config.soft_dir / archive_name
os.makedirs(acc_dir, exist_ok=True) target_zip = config.acc_dir / archive_name
# 4. Move zip file to NovinAcc config.ensure_directories()
print(f"[+] Moving {target_zip_name} to NovinAcc directory...")
shutil.move(source_zip_path, target_zip_path)
print(" [+] Zip file moved successfully.")
# 5. Extract archive if config.dry_run:
print(f"[+] Extracting archive to {acc_dir}...") print(f" [DRY-RUN] Would locate {archive_name} in {config.soft_dir}, move to {config.acc_dir} and extract.")
with zipfile.ZipFile(target_zip_path, 'r') as zip_ref: return config.acc_dir
zip_ref.extractall(acc_dir)
print(" [+] All files extracted successfully.")
# 6. Remove temporary zip file # Check if archive exists in soft_dir or already in acc_dir
if os.path.exists(target_zip_path): archive_to_extract = None
os.remove(target_zip_path) if source_zip.exists():
print(" [+] Temporary zip file deleted.") 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__": if __name__ == "__main__":
os.system("chcp 65001 > nul") import sys
print("[+] Running move and extract module ...") logging.basicConfig(level=logging.INFO, format="%(message)s")
print("[*] Running NovinAcc archive extractor standalone ...")
try: 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: except Exception as e:
print(f"[-] Error: {e}") print(f"[-] Error: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -1,71 +1,107 @@
"""
Extractor and automated installer runner for NovinDesk.
"""
import logging
import os import os
import zipfile from pathlib import Path
import subprocess import subprocess
import time 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: try:
import pyautogui import pyautogui # type: ignore
# 1. Close any secondary/side dialogs # 1. Close any secondary/side dialogs
pyautogui.press('escape') pyautogui.press('escape')
time.sleep(0.5) 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): for _ in range(4):
pyautogui.press('tab') pyautogui.press('tab')
time.sleep(0.2) time.sleep(0.2)
# 3. Press Enter on the install button # 3. Press Enter on the install button
pyautogui.press('enter') 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: 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 return exe_file
if __name__ == "__main__": if __name__ == "__main__":
os.system("chcp 65001 > nul") import sys
print("[+] Running NovinDesk installation module ...") logging.basicConfig(level=logging.INFO, format="%(message)s")
print("[*] Running NovinDesk installation module standalone ...")
try: 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: except Exception as e:
print(f"[-] Error: {e}") print(f"[-] Error: {e}", file=sys.stderr)
sys.exit(1)