Last active
September 6, 2024 13:27
-
-
Save dinhani/465baab2c01246eea6a03d06bdf9f250 to your computer and use it in GitHub Desktop.
Automatically backup saves on change
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
set input="C:\Program Files (x86)\Steam\steamapps\common\STALKER Clear Sky\_appdata_\savedgames\renato_quicksave.sav" | |
set output="C:\Program Files (x86)\Steam\steamapps\common\STALKER Clear Sky\_appdata_\savedgames" | |
set freq=10 | |
set max=50 | |
wt -p "PS7" powershell.exe -NoExit -Command "python D:/codigo/automacao/backup-saves.py --input '%input%' --output '%output%' --freq %freq% --max %max%" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ------------------------------------------------------------------------------ | |
# imports | |
# ------------------------------------------------------------------------------ | |
from datetime import datetime | |
from PIL import ImageGrab | |
from simple_chalk import green, yellow | |
import argparse | |
import glob | |
import ntpath | |
import os | |
import shutil | |
import sys | |
import time | |
# ------------------------------------------------------------------------------ | |
# constants | |
# ------------------------------------------------------------------------------ | |
HOUR_FORMAT = "%H:%M:%S" | |
# ------------------------------------------------------------------------------ | |
# functions - parsing | |
# ------------------------------------------------------------------------------ | |
class Args: | |
def __init__(self, args): | |
self.saves = args.input | |
self.backup_folder = args.output | |
self.backup_to_keep = int(args.max) | |
self.freq_in_secs = int(args.freq) | |
def __str__(self) -> str: | |
repr = "ARGS:\n" | |
repr += f" SAVES = {self.saves}\n" | |
repr += f" BACKUP_FOLDER = {self.backup_folder}\n" | |
repr += f" BACKUP_TO_KEEP = {self.backup_to_keep}\n" | |
repr += f" FREQ_IN_SECS = {self.freq_in_secs}\n" | |
return repr | |
parser = argparse.ArgumentParser(prog="Backup Saves", description="Backup game saves automatically") | |
parser.add_argument("-i", "--input", required=True, action="append", help="Game saves folder or files") | |
parser.add_argument("-o", "--output", required=True, help="Backup folder") | |
parser.add_argument("-m", "--max", required=False, default=500, help="Number of backups to keep before old ones are removed") | |
parser.add_argument("-f", "--freq", required=False, default=60, help="Frequency of the backups (in seconds)") | |
# ------------------------------------------------------------------------------ | |
# functions - backup | |
# ------------------------------------------------------------------------------ | |
def backup_if_necessary(args: Args, save_filename: str, save_last_change: float): | |
current_change = save_last_change | |
try: | |
current_change = read_change_time(save_filename) | |
if should_backup(current_change, save_last_change): | |
backup_filename = execute_backup(save_filename, args.backup_folder) | |
execute_screenshot(backup_filename) | |
backup_to_keep = args.backup_to_keep if os.path.isdir(backup_filename) else args.backup_to_keep * 2 # screenshots doubles the number of files to keep | |
execute_delete_old_backups(args.backup_folder, backup_to_keep) | |
print("") | |
except Exception as e: | |
line = sys.exc_info()[2].tb_lineno | |
print(f"Error at {line}: {e}") | |
return current_change | |
def read_change_time(save_filename: str) -> float: | |
# if file, read the change time from the file | |
if os.path.isfile(save_filename): | |
mtime = os.path.getmtime(save_filename) | |
return mtime | |
# if folder, iterate the entire tree and find the most recent change of all files | |
else: | |
most_recent_filename = save_filename | |
most_recent_change = 0 | |
for root, _, files in os.walk(save_filename): | |
for file in files: | |
save_filename = os.path.join(root, file) | |
save_change = read_change_time(save_filename) | |
if save_change > most_recent_change: | |
most_recent_filename = save_filename | |
most_recent_change = save_change | |
print(f"RECENT = {most_recent_filename}") | |
return most_recent_change | |
def should_backup(current_change: float, last_change: float | None) -> bool: | |
changed = last_change is None or current_change > last_change | |
# log | |
print(f"RECENT = {datetime.fromtimestamp(current_change).strftime(HOUR_FORMAT)}") | |
if last_change is not None: | |
print(f"LAST = {datetime.fromtimestamp(last_change).strftime(HOUR_FORMAT)}") | |
else: | |
print(f"LAST = {last_change}") | |
if changed: | |
print("") | |
print(green("BACKUP")) | |
else: | |
print(yellow("IGNORE")) | |
return changed | |
# ------------------------------------------------------------------------------ | |
# functions - backup | |
# ------------------------------------------------------------------------------ | |
def execute_backup(save_filename: str, backup_folder: str) -> str: | |
backup_filename = generate_backup_filename(save_filename, backup_folder) | |
copy_file(save_filename, backup_filename) | |
return backup_filename | |
def generate_backup_filename(save_filename: str, backup_folder: str) -> str: | |
save_filename = ntpath.basename(save_filename) | |
backup_filename = os.path.join(backup_folder, "{}--{}".format(datetime.today().strftime("%Y%m%d_%H%M%S"), save_filename)) | |
return backup_filename | |
def copy_file(save_filename: str, backup_filename: str) -> str: | |
print(f"TIME = {datetime.now().strftime(HOUR_FORMAT)}") | |
print(f"SOURCE = {save_filename}") | |
print(f"BACKUP = {backup_filename}") | |
if os.path.isfile(save_filename): | |
shutil.copyfile(save_filename, backup_filename) | |
else: | |
shutil.copytree(save_filename, backup_filename) | |
# ------------------------------------------------------------------------------ | |
# functions - screenshot | |
# ------------------------------------------------------------------------------ | |
def execute_screenshot(backup_filename: str): | |
screenshot_filename = generate_screenshot_filename(backup_filename) | |
print(f"SCREEN = {screenshot_filename}") | |
screenshot = ImageGrab.grab(all_screens = False) | |
screenshot.save(screenshot_filename) | |
screenshot.close() | |
def generate_screenshot_filename(backup_filename: str) -> str: | |
if os.path.isfile(backup_filename): | |
screenshot_filename = os.path.splitext(backup_filename)[0] | |
return screenshot_filename + "--screenshot.png" | |
else: | |
return os.path.join(backup_filename, "_screenshot.png") | |
# ------------------------------------------------------------------------------ | |
# functions - delete old backups | |
# ------------------------------------------------------------------------------ | |
def execute_delete_old_backups(backup_folder: str, backups_to_keep: int): | |
backups_to_delete = get_backups_to_delete(backup_folder, backups_to_keep) | |
for backup_to_delete in backups_to_delete: | |
delete_file(backup_to_delete) | |
def get_backups_to_delete(backup_folder: str, backups_to_keep: int): | |
backup_glob = os.path.join(backup_folder, "**--**") | |
backups = sorted(glob.glob(backup_glob), reverse=True) | |
if len(backups) > backups_to_keep: | |
return backups[backups_to_keep:len(backups)] | |
else: | |
return [] | |
def delete_file(backup_filename): | |
print(f"DELETE = {backup_filename}") | |
if os.path.isfile(backup_filename): | |
os.remove(backup_filename) | |
else: | |
shutil.rmtree(backup_filename) | |
# ============================================================================== | |
# main | |
# ============================================================================== | |
# parse args | |
args = Args(parser.parse_args()) | |
print("") | |
print(args) | |
# init last change | |
saves_last_change = [] | |
for i, save in enumerate(args.saves): | |
print(f"WATCH = {save}") | |
print(" ") | |
saves_last_change.append(None) | |
# create backup dir | |
if not os.path.exists(args.backup_folder): | |
os.makedirs(args.backup_folder) | |
# keep checking if save changed | |
while True: | |
for i, save in enumerate(args.saves): | |
save_last_change = saves_last_change[i] | |
saves_last_change[i] = backup_if_necessary(args, save, save_last_change) | |
# wait for next check | |
time.sleep(args.freq_in_secs) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment