Skip to content

Instantly share code, notes, and snippets.

@dinhani
Last active September 6, 2024 13:27
Show Gist options
  • Save dinhani/465baab2c01246eea6a03d06bdf9f250 to your computer and use it in GitHub Desktop.
Save dinhani/465baab2c01246eea6a03d06bdf9f250 to your computer and use it in GitHub Desktop.
Automatically backup saves on change
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%"
# ------------------------------------------------------------------------------
# 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