ftl-save-manager/ftl-savemanager.py
Mr Finchum 7e8a68e73e Feature/updater can now update from gitlab
Added update function, update function does not work inside git folder.
2024-12-03 22:50:34 +00:00

270 lines
9.5 KiB
Python

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