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

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

View File

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

View File

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