import os import time import sys import requests from datetime import datetime version = "0.3.1" gitlab_url = "https://gitlab.com/python_projects3802849/ftl-save-manager/-/raw/main" settings_location = "settings.txt" 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): """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): """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 list_backup(self): """"List all files inside the backup folder.""" files = os.listdir(self.backup_path) files.reverse() # this way the newest file is the top one. self.backup_files.clear() i = 1 for file in files: new_item = {f"{i}": file} print(f"[{i}] {file}") self.backup_files.update(new_item) i += 1 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(self): """Copies a selected backup file to the game folder and renames it.""" print("Select a file to restore (back [b]):") self.list_backup() while True: choice = input("Enter number (b = back): ") if choice in self.backup_files: self.copy_file(f"{self.backup_path}/{self.backup_files[choice]}", f"{self.game_path}/continue.sav") print(f"{self.backup_files[choice]} restored.") time.sleep(0.5) break elif choice == "b": break else: print("not a valid option") continue def delete_backup_file(self): """Deltes all .bkup files in the backup folder.""" # Making a yes/no function would be cleaner 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): """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 SimpleUI: """'UI for the backup class, simply displayes a menu and accepts letter to choice option.""" def __init__(self, options, names): self.options = options self.names = names def menu(self): """"Use dict to select an option (function).""" self.print_menu() while True: choice = input("Select Option: ") print("\n") if choice in self.options: self.options[choice]() self.print_menu() else: print("Try again") continue def print_menu(self): #print("\033[H\033[J", end="") print("Menu options") for key in self.options: print(f"{key}: {self.names[key]}") class Updater: """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.\nCurrent version: {self.current_version}") return True else: print("You are already on the latest version.") 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): """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.") 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): """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") up = Updater(version, gitlab_url, "ftl-savemanager.py") app = BackupApp(settings_location) # Designing the menu options from the backupapp and the layout menu_options = { "b": app.backup, "r": app.restore, "d": app.delete_backup_file, "u": up.run_update, "q": exit } menu_names = { "b": "Backup now", "r": "Restore backup", "d": "Delete backup files", "u": "Update", "q": "Exit" } # Loading the ui and the options sui = SimpleUI(menu_options, menu_names) # starting the ui i.e. program sui.menu()