import os import time import sys from datetime import datetime version = "0.4.2" gitlab_url = "https://gitlab.com/python_projects3802849/ftl-save-manager/-/raw/main" settings_location = "settings.txt" dependencies = ("simple_term_menu", "requests") if sys.platform != "linux": print(f"{sys.platform} is not supported, only linux.") exit() def check_package_installed(package_name): """Check if a package is installed.""" try: __import__(package_name) return True except ImportError as e: print(f"{package_name} not found. Error: {e}") return False for item in dependencies: if check_package_installed(item) == False: print(f"One or more dependencies are missing, please ensure follwing packaged are installed:\nrequests\nsimple-term-menu") exit() import requests # todo: add check if installed, if not disable update function. from simple_term_menu import TerminalMenu class BackupApp: """"Backup class, copy FTL continue file to backup location and restores it.""" def __init__(self, settings_file_path): self.backup_files = {"1": "initiating"} # initiating variable self.settings_path = settings_file_path self.settings = self.read_settings() if self.settings: self.game_path = self.settings[0] self.backup_path = self.settings[1] else: self.game_path, self.backup_path = self.ask_for_path() self.write_settings([self.game_path, self.backup_path]) print(f"Game location: {self.game_path}.\nSave location: {self.backup_path}") def read_settings(self): """Reading the settings file.""" if os.path.exists(self.settings_path): with open(self.settings_path, "r") as f: data = f.readlines() for i in range(len(data)): data[i] = data[i].strip() #stripping white space return(data) else: print("settings file does not exit.") return False def ask_for_path(self): """Uses get_path to request path from the user.""" game_path = self.get_path("Game save path", True) # backup_path = self.get_path("Backup path") return(game_path, backup_path) def get_path(self, str, ftl = False): # improvment: function that checks path or file """Get path from user and check if exists.""" while True: path = input(f"please enter {str}: ") if ftl: profile_file = path + "/ae_prof.sav" if os.path.isfile(profile_file): return path else: print("Path is not valid, try again.") continue if os.path.exists(path): return path break else: print("Path not valid, try again.") continue return path def write_settings(self, list): # improvment: more generic name, write_file or write_list_as_file """Write the settings (path)""" with open(self.settings_path, "w") as f: for item in list: f.write(item + "\n") print("write settings.") def copy_file(self, src, dst): """"Simply copies a file from a to b.""" if not os.path.isfile(src): print(f"File to copy {src} does not exist.") return False os.system(f"cp {src} {dst}") print(f"Wrote: {dst}") def backup(self): """Copies the game file to the backup folder and adds date-time stamp.""" now = datetime.now() formatted_now = now.strftime("%Y-%m-%d_%H-%M") self.copy_file(f"{self.game_path}/continue.sav", f"{self.backup_path}/{formatted_now}.bkup") time.sleep(0.5) def restore_backup(self, file): self.copy_file(f"{self.backup_path}/{file}", f"{self.game_path}/continue.sav") def delete_backup_file(self): # improvment: here also, more generic, delete_file. """Deltes all .bkup files in the backup folder.""" if self.yes_no("Backup"): for file in os.listdir(self.backup_path): if file[-5:] == ".bkup": try: print(f"Deleting {file}.") os.remove(f"{self.backup_path}/{file}") time.sleep(0.3) except Exception as e: print(f"Failed to delete {file}. Reason: {e}.") #time.sleep(2) def yes_no(self, str): # improvment: more generic, take two string. """Ask user y/n question regaring deletion of a string.""" while True: choice = input(f"Are you sure you want to delete all {str} files? (y/n)") if choice == "y": return True elif choice == "n": print(f"Deleting {str} files aborted") #time.sleep(1) return False else: print("Not a valid option, try again") class Updater: # improvment: if dependency not aviable disable this """Update local version from gitlab repo.""" def __init__(self, version, update_url, local_file): self.current_version = version self.update_url = update_url self.local_file = local_file self.git = os.path.exists(".gitignore") # Checking if the program inside git env def check_for_update(self): """Check if a new version is available.""" try: print("Checking for updates...") response = requests.get(f"{self.update_url}/latest_version.txt") response.raise_for_status() latest_version = response.text.strip() if self.compare_SemVer(latest_version): print(f"New version {latest_version} available online.\nCurrent version: {self.current_version}") return True else: print(f"You are up-to-date, the latest version is: {latest_version}.") time.sleep(2) return False except Exception as e: print(f"Error checking for updates: {e}") return False def compare_SemVer(self, latest_version): """Compare two Semantic versioning strings.""" local_version = self.convert_str_list_to_int(self.current_version.split(".")) online_version = self.convert_str_list_to_int(latest_version.split(".")) return local_version < online_version def convert_str_list_to_int(self, list_str): """Converts a list with strings to a list with intengers.""" return [int(i) for i in list_str] def download_update(self, latest_version): # improvment: silly it takes the latest_version as variable """Download the new version and overwrite the current file.""" try: print(f"Downloading version {latest_version}...") response = requests.get(f"{self.update_url}/{self.local_file}") response.raise_for_status() with open(self.local_file, "wb") as file: file.write(response.content) print("Update downloaded.") return True except Exception as e: print(f"Error downloading update: {e}") return False def restart_program(self): """Restart the current program.""" print("Restarting the program...") os.execv(sys.executable, ["python"] + sys.argv) def run_update(self): """Main method to check, update, and restart.""" if self.git: print("Updating only works outside git env.") time.sleep(2) else: latest_version = self.check_for_update() if latest_version: if self.yes_no("Do you want to update the program?"): success = self.download_update(latest_version) if success: self.restart_program() else: print("Update cancelled") return def yes_no(self, str): # improvment: have two yes_no function, should merge them """Ask user y/n question""" while True: choice = input(f"{str} (y/n)") if choice == "y": return True elif choice == "n": return False else: print("Not a valid option, try again") class TerminalUI: def __init__(self, title, options): self.main_menu_title = title self.main_menu_options = options self.main_menu_keys = list(self.main_menu_options.keys()) # terminal ui takes a list for the menu self.main_menu = TerminalMenu( menu_entries = self.main_menu_keys, title = self.main_menu_title, menu_cursor = "> ", menu_cursor_style = ("fg_gray", "bold"), menu_highlight_style = ("bg_gray", "fg_black"), cycle_cursor = True, clear_screen = True, ) def show_main_menu(self): while True: selected_index = self.main_menu.show() selected_key = None if type(selected_index) == int: selected_key = self.main_menu_keys[selected_index] if selected_index == None: break elif selected_key == "[r] Restore backup": self.restore_file_menu(app.backup_path) elif selected_key in self.main_menu_options: self.main_menu_options[selected_key]() def restore_file_menu(self, folder): menu_options = list(self.list_files(folder)) folder_menu = TerminalMenu(menu_entries = menu_options, title = f"Content of {folder} (Q or Esc to go back.)\n", menu_cursor = "> ", menu_cursor_style = ("fg_red", "bold"), menu_highlight_style = ("bg_gray", "fg_black"), cycle_cursor = True, clear_screen = True) menu_entry_index = folder_menu.show() if menu_entry_index == None: return else: app.restore_backup(menu_options[menu_entry_index]) def list_files(self, directory): return (file for file in sorted(os.listdir(directory), reverse = True) if os.path.isfile(os.path.join(directory, file)) and file.endswith((".bkup"))) def yes_no_menu(self, message): # oh menu_options = ["[y] yes", "[n] no"] menu = TerminalMenu(menu_entries = menu_options, title = f"{message}", menu_cursor = "> ", menu_cursor_style = ("fg_red", "bold"), menu_highlight_style = ("bg_gray", "fg_black"), cycle_cursor = True, clear_screen = True) menu_entry_index = menu.show() if menu_entry_index == 0: return True elif menu_entry_index == 1: return False up = Updater(version, gitlab_url, "ftl-savemanager.py") app = BackupApp(settings_location) main_menu_options = { "[b] Backup now": app.backup, "[r] Restore backup": "restore_file_menu", "[d] Delete backup files": app.delete_backup_file, "[u] Look for update": up.run_update, "[q] Quit": exit } title = f"ftl-savemanager v{version} (Press Q or Esc to quit).\n" tui = TerminalUI(title, main_menu_options) tui.show_main_menu()