python/steamsync: Add support for using Steam Cloud UFS metadata

master
Taeyeon Mori 4 years ago
parent eaca8a670e
commit 9297304cfd
  1. 2
      bin/protontool
  2. 7
      bin/sync_savegames
  3. 117
      lib/python/steamsync.py

@ -50,7 +50,7 @@ class ProtonTool:
else:
apps = list(self.steam.find_apps(args.game, installed=not args.show_all))
if len(apps) > 1:
print_error("Found more than one installed game matching /%s/, please be more specific" % args.game)
print_error("Found more than one game matching /%s/, please be more specific" % args.game)
for app in apps:
print("\t", app)
sys.exit(2)

@ -51,6 +51,13 @@ def sacred(op):
set.execute()
@sync.by_id(39540) # SpellForce Platinum
def spellforce(op):
sync = op.steam_cloud_ufs()
if sync.show_confirm():
sync.execute()
### -----------------------------------------------------------------
# Other Games
### -----------------------------------------------------------------

@ -5,6 +5,8 @@ import sys, os
import fnmatch
import re
import itertools
import functools
import operator
import shutil
import tarfile
import time
@ -50,7 +52,7 @@ class SyncPath:
""" Return a new SyncPath that has a component prefixed to the local path """
return SyncPath(self.op, self.local / component, self.common)
def __div__(self, component: Union[str, Path]) -> 'SyncPath':
def __truediv__(self, component: Union[str, Path]) -> 'SyncPath':
""" Return a new SyncPath that nas a component added """
return SyncPath(self.op, self.local, self.common / component)
@ -78,7 +80,25 @@ class SyncPath:
pass
class SyncSet:
class _SyncSetCommon:
def show_confirm(self, skip=True) -> bool:
# XXX: move to SyncOp?
print(" Local is newer: ", ", ".join(map(str, self.files_from_local)))
print(" Target is newer: ", ", ".join(map(str, self.files_from_target)))
print(" Unmodified: ", ", ".join(map(str, self.files_unmodified)))
if skip and not self.files_from_local and not self.files_from_target:
print(" \033[31mNoting to do!\033[0m")
return False
print("Continue? <Y/n> ", end="")
resp = input().strip()
if resp.lower() in ("y", "yes", ""):
return True
return False
class SyncSet(_SyncSetCommon):
"""
A SyncSet represents a set of files to be synchronized
from a local to a target location represented by a SyncPath
@ -161,22 +181,6 @@ class SyncSet:
def files_unmodified(self) -> Set[Path]:
return (self.local.keys() | self.target.keys()) - (self.files_from_local | self.files_from_target)
def show_confirm(self, skip=True) -> bool:
# XXX: move to SyncOp?
print(" Local is newer: ", ", ".join(map(str, self.files_from_local)))
print(" Target is newer: ", ", ".join(map(str, self.files_from_target)))
print(" Unmodified: ", ", ".join(map(str, self.files_unmodified)))
if skip and not self.files_from_local and not self.files_from_target:
print(" \033[31mNoting to do!\033[0m")
return False
print("Continue? <Y/n> ", end="")
resp = input().strip()
if resp.lower() in ("y", "yes", ""):
return True
return False
def backup(self):
if not self.files_from_local:
print(" \033[35mBackup not needed\033[0m")
@ -203,6 +207,30 @@ class SyncSet:
return self.op._do_copy(operations)
class SyncMultiSet(list, _SyncSetCommon):
""" Provides a convenient interface to a number of SyncSets """
def _union_set(self, attrname) -> Set[Path]:
if not self:
return set()
return functools.reduce(operator.or_, map(operator.attrgetter(attrname), self))
@property
def files_from_local(self) -> Set[Path]:
return self._union_set("files_from_local")
@property
def files_from_target(self) -> Set[Path]:
return self._union_set("files_from_target")
@property
def files_unmodified(self) -> Set[Path]:
return self._union_set("files_unmodified")
def execute(self, *, make_inconsistent=False) -> bool:
for sset in self:
sset.execute(make_inconsistent=make_inconsistent)
### -----------------------------------------------------------------
# Sync Operation
### -----------------------------------------------------------------
@ -321,6 +349,57 @@ class SteamSyncOp(AbstractSyncOp):
else:
return super().my_documents
@property
def user_home(self) -> SyncPath:
return SyncPath(self, self.parent.home_path)
## Steam Cloud
def steam_cloud_ufs(self) -> SyncMultiSet:
if "ufs" not in self.app.appinfo["appinfo"] or "savefiles" not in self.app.appinfo["appinfo"]["ufs"]:
raise ValueError("%r doesn't support Steam Cloud by way of UFS" % self.app)
sms = SyncMultiSet()
if sys.platform.startswith("win") or self.app.is_proton_app:
ufs_platform = "Windows"
elif sys.platform.startswith("linux"):
ufs_platform = "Linux"
else:
raise NotImplementedError("Steam Cloud UFS not (yet) supported on platform %s" % sys.platform)
for ufs_def in self.app.appinfo["appinfo"]["ufs"]["savefiles"].values():
# Filter by platform
if "platforms" in ufs_def and ufs_platform not in ufs_def["platforms"].values():
continue
# Find root anchor
root = ufs_def["root"]
if root == "WinMyDocuments":
path = self.my_documents
elif root in ("LinuxHome", "MacHome"):
path = self.user_home
else:
raise NotImplementedError("Steam Cloud UFS root %s not implemented for %r" % (root, self.app))
# Add relative path
# XXX: Should path be prefixed or included in the target. Are there even apps with multiple ufs entries?
# For now, take last component?
if "path" in ufs_def and ufs_def["path"]:
rpath = Path(ufs_def["path"])
if rpath.anchor:
# Fix paths with leading slash/backslash XXX: is this valid?
rpath = rpath.relative_to(rpath.anchor)
if len(rpath.parts) > 1:
path = path.prefix(rpath.parent)
path /= rpath.name
# Add files by pattern
sset = SyncSet(path)
sset.add(ufs_def["pattern"])
# XXX: what about platform and recursive keys?
sms.append(sset)
return sms
class SyncNoOp:
""" No-Op Sync Operation """
@ -344,7 +423,7 @@ class SteamSync:
def __init__(self, target_path: Path, *, steam_path: Path = None):
self.target_path = Path(target_path)
self.steam = Steam(steam_path)
self.home_path = Path("~").expanduser()
self.home_path = Path.home()
# Get Information
@CachedProperty

Loading…
Cancel
Save