diff --git a/nandfixpro.py b/nandfixpro.py index 28e0fd8..661d493 100644 --- a/nandfixpro.py +++ b/nandfixpro.py @@ -1581,16 +1581,68 @@ def _save_config(self): config.write(configfile) self._log(f"INFO: Configuration saved to {self.config_file}") - def _is_path_valid(self, key): - """Helper to check if a path from the dict is non-empty and exists.""" - path_str = self.paths[key].get() - if not path_str: - return False - return Path(path_str).exists() - - def _check_disk_space(self, required_gb=60): - try: - import shutil + def _is_path_valid(self, key): + """Helper to check if a path from the dict is non-empty and exists.""" + path_str = self.paths[key].get() + if not path_str: + return False + return Path(path_str).exists() + + def _get_app_base_dir(self): + """Return the actual app directory (stable for both script and packaged exe).""" + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + try: + return Path(__file__).resolve().parent + except NameError: + return Path.cwd().resolve() + + def _get_partitions_folder(self): + """Resolve donor NAND partitions folder from settings with a safe fallback.""" + configured_path = self.paths['partitions_folder'].get().strip() + if configured_path: + partitions_folder = Path(configured_path) + if partitions_folder.is_dir(): + return partitions_folder + self._log(f"ERROR: Partitions folder path is invalid: {partitions_folder}") + return None + + fallback_folder = self._get_app_base_dir() / "lib" / "NAND" + if fallback_folder.is_dir(): + self._log(f"WARNING: Partitions folder not set, using fallback: {fallback_folder}") + return fallback_folder + + self._log("ERROR: Partitions folder is not configured and fallback lib/NAND was not found.") + return None + + def _estimate_nand_size_gb(self, nand_source, source_type): + """Estimate NAND source size in GB from file or physical device path.""" + try: + if source_type == 'file': + source_path = Path(nand_source) + if source_path.is_file(): + return source_path.stat().st_size / (1024**3) + elif source_type == 'device': + size_bytes = self._get_disk_size_bytes(nand_source) + if size_bytes and size_bytes > 0: + return size_bytes / (1024**3) + except Exception as e: + self._log(f"WARNING: Could not estimate NAND size for {nand_source}: {e}") + return None + + def _required_temp_space_gb(self, nand_size_gb=None): + """ + Required free temp space: + - 32GB NAND class -> 64GB free + - 64GB NAND class (OLED) -> 128GB free + """ + if nand_size_gb is not None and nand_size_gb > 40: + return 128 + return 64 + + def _check_disk_space(self, required_gb=60): + try: + import shutil # Use custom temp directory if set, otherwise use system default if self.paths['temp_directory'].get(): temp_dir = self.paths['temp_directory'].get() @@ -1755,18 +1807,12 @@ def _show_about_window(self): "Developed and maintained by: sthetix") CustomDialog(self, title="About NAND Fix Pro", message=about_message) - def _show_usage_guide_window(self): - """Creates a new window and displays the contents of usage.txt.""" - try: - # Determine the path to the usage guide - try: - # Path when running as a script - base_path = Path(__file__).parent - except NameError: - # Path when running as a frozen executable (PyInstaller) - base_path = Path(sys.executable).parent - - guide_path = base_path / "lib" / "docs" / "usage.txt" + def _show_usage_guide_window(self): + """Creates a new window and displays the contents of usage.txt.""" + try: + # Determine the path to the usage guide from the app folder + base_path = self._get_app_base_dir() + guide_path = base_path / "lib" / "docs" / "usage.txt" if guide_path.is_file(): with open(guide_path, 'r', encoding='utf-8') as f: @@ -1950,9 +1996,8 @@ def _reset_application_state(self): self._log("--- Application has been reset. Ready for a new operation. ---") self._validate_paths_and_update_buttons() - def _auto_detect_paths(self): - try: script_dir = Path(__file__).parent - except NameError: script_dir = Path.cwd() + def _auto_detect_paths(self): + script_dir = self._get_app_base_dir() osfmount_path = Path("C:/Program Files/OSFMount/OSFMount.com") if osfmount_path.is_file(): self.paths["osfmount"].set(str(osfmount_path.resolve())) @@ -2284,15 +2329,14 @@ def _run_user_fix_process(self): nand_target = drive_path self._log(f"--- SUCCESS: User confirmed eMMC at {drive_path}") - # STEP 2: Extract the correct USER partition - self._log("\n[STEP 2/4] Preparing donor USER partition...") - try: - script_dir = Path(__file__).parent - except NameError: - script_dir = Path.cwd() - partitions_folder = script_dir / "lib" / "NAND" - - user_archive = "USER-64.7z" if target_size_gb > 40 else "USER-32.7z" + # STEP 2: Extract the correct USER partition + self._log("\n[STEP 2/4] Preparing donor USER partition...") + partitions_folder = self._get_partitions_folder() + if not partitions_folder: + self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.") + return + + user_archive = "USER-64.7z" if target_size_gb > 40 else "USER-32.7z" cmd = [self.paths['7z'].get(), 'x', str(partitions_folder / user_archive), f'-o{temp_dir}', '-bsp1', '-y'] if self._run_command_with_progress(cmd, "Extracting USER partition")[0] != 0: @@ -2674,17 +2718,14 @@ def _selective_copy_system_contents(self, source_system_path, drive_letter): self._log(traceback.format_exc()) return False - def _get_donor_nand_path(self, target_size_gb, temp_dir): - """ - Automatically detect and extract the appropriate donor NAND based on eMMC size. - Returns the path to the extracted donor NAND image. - """ - try: - script_dir = Path(__file__).parent - except NameError: - script_dir = Path.cwd() - - nand_lib_dir = script_dir / "lib" / "NAND" + def _get_donor_nand_path(self, target_size_gb, temp_dir): + """ + Automatically detect and extract the appropriate donor NAND based on eMMC size. + Returns the path to the extracted donor NAND image. + """ + nand_lib_dir = self._get_partitions_folder() + if not nand_lib_dir: + return None if target_size_gb > 40: donor_archive, donor_bin_name, size = (nand_lib_dir / "donor64.7z", "rawnand64.bin", "64GB") @@ -2895,13 +2936,10 @@ def _run_level3_process(self, temp_dir): if self.offline_mode.get(): self._log("\n--- OFFLINE MODE ---") self._log("Level 3 will create a completely reconstructed NAND file.") - else: - self._log("\n--- WARNING ---") - self._log("Level 3 will completely overwrite your Switch's eMMC with a reconstructed NAND.") - self._log("This is irreversible. Ensure you have backups and a stable connection.") - - if not self._check_disk_space(60): - return + else: + self._log("\n--- WARNING ---") + self._log("Level 3 will completely overwrite your Switch's eMMC with a reconstructed NAND.") + self._log("This is irreversible. Ensure you have backups and a stable connection.") # Determine target size for offline mode or get from physical eMMC if self.offline_mode.get(): @@ -2958,13 +2996,18 @@ def _run_level3_process(self, temp_dir): "WARNING: ALL DATA ON THIS DRIVE WILL BE PERMANENTLY ERASED.\n\n" "This will perform a complete Level 3 recovery. Continue?") - if not self._dialog("Confirm Level 3 Recovery", msg, buttons="yesno"): - self._log("--- User cancelled Level 3 recovery.") - return - - self._log(f"SUCCESS: User confirmed target eMMC at {target_path} ({target_drive['size']})") - - self._log(f"\n[STEP 2/8] Preparing donor NAND skeleton...") + if not self._dialog("Confirm Level 3 Recovery", msg, buttons="yesno"): + self._log("--- User cancelled Level 3 recovery.") + return + + self._log(f"SUCCESS: User confirmed target eMMC at {target_path} ({target_drive['size']})") + + required_space = self._required_temp_space_gb(target_size_gb) + self._log(f"--- Target NAND size: {target_size_gb:.1f}GB. Required temp free space: {required_space}GB.") + if not self._check_disk_space(required_space): + return + + self._log(f"\n[STEP 2/8] Preparing donor NAND skeleton...") # Automatically detect and extract donor NAND skeleton based on target eMMC size donor_nand_path = self._get_donor_nand_path(target_size_gb, temp_dir) @@ -3026,9 +3069,12 @@ def _run_level3_process(self, temp_dir): self._log("ERROR: No EmmcHaccGen output folder found.") return - self._log(f"\n[STEP 5/8] Preparing all partition data from donor archives...") - nx_exe = self.paths['nxnandmanager'].get() - partitions_folder = Path(self.paths['partitions_folder'].get()) + self._log(f"\n[STEP 5/8] Preparing all partition data from donor archives...") + nx_exe = self.paths['nxnandmanager'].get() + partitions_folder = self._get_partitions_folder() + if not partitions_folder: + self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.") + return # --- MODIFIED FOR V1.0.2: Progress Bar for all 7z extractions --- for part_info in [("SYSTEM", "SYSTEM.7z"), ("PRODINFOF", "PRODINFOF.7z"), ("SAFE", "SAFE.7z")]: @@ -3806,28 +3852,28 @@ def _get_nand_source(self): self._log(f"SUCCESS: User confirmed eMMC at {drive_path}") return drive_path, 'device' - def _run_level1_process(self, temp_dir): - if self.offline_mode.get(): - self._log("\n--- OFFLINE MODE ---") - self._log("The Level 1 process will create a fixed NAND file.") - else: - self._log("\n--- WARNING ---") - self._log("The Level 1 process will write directly to your Switch's eMMC.") - - if not self._check_disk_space(60): - return - - # Get NAND source (file or device) - if not self.offline_mode.get(): - self._log("\n[STEP 1/8] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).") - else: - self._log("\n[STEP 1/8] Using NAND file(s) from settings...") + def _run_level1_process(self, temp_dir): + if self.offline_mode.get(): + self._log("\n--- OFFLINE MODE ---") + self._log("The Level 1 process will create a fixed NAND file.") + else: + self._log("\n--- WARNING ---") + self._log("The Level 1 process will write directly to your Switch's eMMC.") + + if not self._check_disk_space(60): + return + + # Get NAND source (file or device) + if not self.offline_mode.get(): + self._log("\n[STEP 1/8] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).") + else: + self._log("\n[STEP 1/8] Using NAND file(s) from settings...") - nand_source, source_type = self._get_nand_source() - if not nand_source: - return - - nx_exe = self.paths['nxnandmanager'].get() + nand_source, source_type = self._get_nand_source() + if not nand_source: + return + + nx_exe = self.paths['nxnandmanager'].get() self._log(f"\n[STEP 2/8] Dumping and decrypting PRODINFO from {source_type}...") keyset_path = self.paths['keys'].get() @@ -4022,35 +4068,39 @@ def _run_and_interrupt_flash(self, command, partition_name, target_mb): except Exception as e: self._log(f"FATAL ERROR during interruptible flash: {e}"); return -1 - def _run_level2_process(self, temp_dir): - if self.offline_mode.get(): - self._log("\n--- OFFLINE MODE ---") - self._log("The Level 2 process will create a fixed NAND file.") - else: - self._log("\n--- WARNING ---") - self._log("The Level 2 process will write directly to your Switch's eMMC.") - - if not self._check_disk_space(60): - return - - # Get NAND source (file or device) - if not self.offline_mode.get(): - self._log("\n[STEP 1/7] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).") + def _run_level2_process(self, temp_dir): + if self.offline_mode.get(): + self._log("\n--- OFFLINE MODE ---") + self._log("The Level 2 process will create a fixed NAND file.") + else: + self._log("\n--- WARNING ---") + self._log("The Level 2 process will write directly to your Switch's eMMC.") + + # Get NAND source (file or device) + if not self.offline_mode.get(): + self._log("\n[STEP 1/7] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).") else: self._log("\n[STEP 1/7] Using NAND file(s) from settings...") - nand_source, source_type = self._get_nand_source() - if not nand_source: - return - - nx_exe = self.paths['nxnandmanager'].get() - - try: - script_dir = Path(__file__).parent - except NameError: - script_dir = Path.cwd() - partitions_folder = script_dir / "lib" / "NAND" - keyset_path = self.paths['keys'].get() + nand_source, source_type = self._get_nand_source() + if not nand_source: + return + + nand_size_gb = self._estimate_nand_size_gb(nand_source, source_type) + required_space = self._required_temp_space_gb(nand_size_gb) + if nand_size_gb is not None: + self._log(f"--- Detected NAND size: {nand_size_gb:.1f}GB. Required temp free space: {required_space}GB.") + else: + self._log(f"--- Could not determine NAND size. Using default required temp free space: {required_space}GB.") + if not self._check_disk_space(required_space): + return + + nx_exe = self.paths['nxnandmanager'].get() + partitions_folder = self._get_partitions_folder() + if not partitions_folder: + self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.") + return + keyset_path = self.paths['keys'].get() self._log(f"\n[STEP 2/7] Acquiring PRODINFO from {source_type}...") prodinfo_path = Path(temp_dir) / "PRODINFO" @@ -4262,4 +4312,4 @@ def _run_level2_process(self, temp_dir): # Note: Admin privileges and dependencies are handled by NandFixProLauncher.exe # This allows the main script to remain clean and avoid antivirus false positives app = SwitchGuiApp() - app.mainloop() \ No newline at end of file + app.mainloop()