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: else:
apps = list(self.steam.find_apps(args.game, installed=not args.show_all)) apps = list(self.steam.find_apps(args.game, installed=not args.show_all))
if len(apps) > 1: 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: for app in apps:
print("\t", app) print("\t", app)
sys.exit(2) sys.exit(2)

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

@ -5,6 +5,8 @@ import sys, os
import fnmatch import fnmatch
import re import re
import itertools import itertools
import functools
import operator
import shutil import shutil
import tarfile import tarfile
import time import time
@ -50,7 +52,7 @@ class SyncPath:
""" Return a new SyncPath that has a component prefixed to the local path """ """ Return a new SyncPath that has a component prefixed to the local path """
return SyncPath(self.op, self.local / component, self.common) 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 a new SyncPath that nas a component added """
return SyncPath(self.op, self.local, self.common / component) return SyncPath(self.op, self.local, self.common / component)
@ -78,7 +80,25 @@ class SyncPath:
pass 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 A SyncSet represents a set of files to be synchronized
from a local to a target location represented by a SyncPath from a local to a target location represented by a SyncPath
@ -161,22 +181,6 @@ class SyncSet:
def files_unmodified(self) -> Set[Path]: def files_unmodified(self) -> Set[Path]:
return (self.local.keys() | self.target.keys()) - (self.files_from_local | self.files_from_target) 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): def backup(self):
if not self.files_from_local: if not self.files_from_local:
print(" \033[35mBackup not needed\033[0m") print(" \033[35mBackup not needed\033[0m")
@ -203,6 +207,30 @@ class SyncSet:
return self.op._do_copy(operations) 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 # Sync Operation
### ----------------------------------------------------------------- ### -----------------------------------------------------------------
@ -321,6 +349,57 @@ class SteamSyncOp(AbstractSyncOp):
else: else:
return super().my_documents 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: class SyncNoOp:
""" No-Op Sync Operation """ """ No-Op Sync Operation """
@ -344,7 +423,7 @@ class SteamSync:
def __init__(self, target_path: Path, *, steam_path: Path = None): def __init__(self, target_path: Path, *, steam_path: Path = None):
self.target_path = Path(target_path) self.target_path = Path(target_path)
self.steam = Steam(steam_path) self.steam = Steam(steam_path)
self.home_path = Path("~").expanduser() self.home_path = Path.home()
# Get Information # Get Information
@CachedProperty @CachedProperty

Loading…
Cancel
Save