diff --git a/.gitignore b/.gitignore index 0c6d291..8845be2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ .Trashes ehthumbs.db Thumbs.db -bin_win/* +bin_win32/* +bin_win64/* bin_osx/* -bin_lnx/* +bin_lnx32/* +bin_lnx64 *.pyc diff --git a/README.md b/README.md index 427207e..40651d5 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Airship is a Python-based program to synchronize game saves between clouds, such Before installing and running Airship, you must install these dependencies. -- All functionality depends on having Python version 2.7 or above installed and accessible by typing `python` into your terminal or command line. Your operating system may have Python installed already; to check, type `python -V` into your terminal or command line. Otherwise, [download Python.](https://www.python.org/downloads) -- Steam Cloud functionality depends on having Steam installed, logged in, and running while Airship is running. [Download Steam.](https://store.steampowered.com/about) +- All functionality depends on having Python version 2.7 or above installed and accessible by typing `python` into your terminal or command line. Your operating system may have Python installed already; to check, type `python -V` into your terminal or command line. Otherwise, [download Python](https://www.python.org/downloads). +- Steam Cloud functionality depends on having Steam installed, logged in, and running while Airship is running. [Download Steam](https://store.steampowered.com/about). - iCloud functionality depends on running Airship on OS X 10.10 Yosemite or above, being logged into iCloud, and having iCloud Drive synchronization enabled in System Preferences. Download the latest release by going to 'releases' at the top of the page and going to the most recent release. @@ -14,5 +14,7 @@ Download the latest release by going to 'releases' at the top of the page and go ## Using To use, simply run the `airship.py` script. Note that you must be logged in to your iCloud and Steam accounts and that Steam must be running. +For instructions on how to run Airship on a schedule, see the wiki page [Automatically running Airship](https://github.com/aarzee/airship/wiki/Automatically-running-Airship). + ## Supported games + The Banner Saga ([Steam Cloud](http://store.steampowered.com/app/237990/), [iCloud](https://itunes.apple.com/us/app/banner-saga/id911006986)) diff --git a/airship.py b/airship.py index 0ba98b7..71ccdf9 100644 --- a/airship.py +++ b/airship.py @@ -1,6 +1,7 @@ import os -import sys -import subprocess +import importlib +import re +import time os.chdir(os.path.dirname(os.path.abspath(__file__))) @@ -11,13 +12,71 @@ 'icloudid': 'MQ92743Y4D~com~stoicstudio~BannerSaga' }] -void = open(os.devnull, 'w') +modules = {'steamcloud': None, 'icloud': None} + +for modulename in modules: + module = importlib.import_module(modulename) + if module.init(): + modules[modulename] = module for game in games: - arguments = ['python', 'propeller.py'] + gamemodules = [] + + for modulename in modules: + if modules[modulename] is not None and modulename + 'id' in game: + module = modules[modulename] + + if modulename + 'folder' in game or 'folder' in game: + module.set_folder(game['folder'] if not modulename + 'folder' in game else game[modulename + 'folder']) + + module.set_id(game[modulename + 'id']) + + if module.will_work(): + gamemodules.append(module) + else: + module.shutdown() + + if len(gamemodules) > 1: + filetimestamps = {} + for moduleindex in range(len(gamemodules)): + filenames = gamemodules[moduleindex].get_file_names() + for filename in filenames: + if re.match(game['regex'], filename): + if not filename in filetimestamps: + filetimestamps[filename] = [0] * len(gamemodules) + filetimestamps[filename][moduleindex] = gamemodules[moduleindex].get_file_timestamp(filename) + + for filename in filetimestamps: + newerfilesmayexist = True + highestlowtimestamp = -1 + while newerfilesmayexist: + newerfilesmayexist = False + lowesttimestamp = int(time.time()) + lowesttimestampindex = -1 + for moduleindex in range(len(gamemodules)): + if highestlowtimestamp < filetimestamps[filename][moduleindex] < lowesttimestamp and filetimestamps[filename][moduleindex] > 0: + lowesttimestamp = filetimestamps[filename][moduleindex] + lowesttimestampindex = moduleindex + if lowesttimestampindex != -1: + newerfilesmayexist = True + highestlowtimestamp = lowesttimestamp + originaldata = gamemodules[lowesttimestampindex].read_file(filename) + if originaldata is not None: + for moduleindex in range(len(gamemodules)): + if moduleindex != lowesttimestampindex and filetimestamps[filename][moduleindex] > 0 and gamemodules[moduleindex].read_file(filename) == originaldata: + filetimestamps[filename][moduleindex] = lowesttimestamp - for property in game: - arguments.append('--' + property) - arguments.append(game[property]) + highesttimestamp = -1 + highesttimestampindex = -1 + for moduleindex in range(len(gamemodules)): + if filetimestamps[filename][moduleindex] > highesttimestamp: + highesttimestamp = filetimestamps[filename][moduleindex] + highesttimestampindex = moduleindex + highesttimestampdata = gamemodules[highesttimestampindex].read_file(filename) + if highesttimestampdata is not None: + for moduleindex in range(len(gamemodules)): + if moduleindex != highesttimestampindex and filetimestamps[filename][moduleindex] < highesttimestamp: + gamemodules[moduleindex].write_file(filename, highesttimestampdata) - subprocess.call(arguments, stdout=void, stderr=void) + for module in gamemodules: + module.shutdown() diff --git a/icloud.py b/icloud.py index dd8b098..3a6e659 100644 --- a/icloud.py +++ b/icloud.py @@ -1,5 +1,21 @@ -import os import platform +import distutils.version +import os + +def init(): + global icloudbundleid + icloudbundleid = None + global icloudfolder + icloudfolder = None + global icloudpath + icloudpath = None + + if platform.system() == 'Darwin': + version, _, machine = platform.mac_ver() + version = distutils.version.StrictVersion(version) + return (machine.startswith('iP') and version > distutils.version.StrictVersion('8')) or version > distutils.version.StrictVersion('10.10') and os.path.isdir(os.path.expanduser('~/Library/Mobile Documents')) + + return False def set_id(bundleid): global icloudbundleid @@ -10,21 +26,18 @@ def set_folder(folder): icloudfolder = folder def will_work(): - if platform.mac_ver()[0].startswith('10.10') and os.path.isdir(os.path.expanduser('~/Library/Mobile Documents')): - global icloudpath - if 'icloudfolder' in globals(): - icloudpath = os.path.expanduser('~/Library/Mobile Documents/' + icloudbundleid + '/' + icloudfolder) - else: - icloudpath = os.path.expanduser('~/Library/Mobile Documents/' + icloudbundleid) - if not os.path.isdir(icloudpath): - os.path.mkdirs(icloudpath) - return True - return False + global icloudpath + icloudpath = os.path.expanduser('~/Library/Mobile Documents/' + icloudbundleid + ('' if icloudfolder is None else '/' + icloudfolder)) + + if not os.path.isdir(icloudpath): + os.makedirs(icloudpath) + + return True def get_file_names(): filenames = [] def recursive_dir_contents(dir): - dircontents = os.listdir(icloudpath if dir is None else icloudpath + '/' + dir ) + dircontents = os.listdir(icloudpath if dir is None else icloudpath + '/' + dir) for item in dircontents: if os.path.isdir(icloudpath + '/' + item if dir is None else icloudpath + '/' + dir + '/' + item): recursive_dir_contents(item if dir is None else dir + '/' + item) @@ -43,5 +56,17 @@ def read_file(filename): return data def write_file(filename, data): - with open(icloudpath + '/' + filename, 'w') as fileobject: + path = icloudpath + '/' + filename + dir = path[:path.rfind('/')] + + if not os.path.isdir(dir): + os.makedirs(dir) + + with open(path, 'w') as fileobject: fileobject.write(data) + +def shutdown(): + global icloudbundleid + icloudbundleid = None + global icloudfolder + icloudfolder = None diff --git a/propeller.py b/propeller.py deleted file mode 100644 index 47726ef..0000000 --- a/propeller.py +++ /dev/null @@ -1,83 +0,0 @@ -import argparse -import sys -import os -import importlib -import re -import time -import hashlib - -parser = argparse.ArgumentParser(description='Helper process for airship.py. Not meant for manual use.') -parser.add_argument('--regex', required=True) -parser.add_argument('--folder') - -possiblemodules = ['icloud', 'steamcloud'] - -for modulename in possiblemodules: - parser.add_argument('--' + modulename + 'id') - parser.add_argument('--' + modulename + 'folder') - -arguments = vars(parser.parse_args(sys.argv[1:])) - -modules = [] - -def add_module(modulename): - try: - module = importlib.import_module(modulename) - - if arguments[modulename + 'folder'] is not None or arguments['folder'] is not None: - module.set_folder(arguments[modulename + 'folder'] if arguments[modulename + 'folder'] is not None else arguments['folder']) - - module.set_id(arguments[modulename + 'id']) - - if module.will_work(): - modules.append(module) - except: - pass - -for modulename in possiblemodules: - if arguments[modulename + 'id'] is not None: - add_module(modulename) - -filetimestamps = {} - -if len(modules) > 1: - for moduleindex in range(len(modules)): - filenames = modules[moduleindex].get_file_names() - for filename in filenames: - if re.match(arguments['regex'], filename): - if not filename in filetimestamps: - filetimestamps[filename] = [0] * len(modules) - filetimestamps[filename][moduleindex] = modules[moduleindex].get_file_timestamp(filename) - - for filename in filetimestamps: - newerfilesmayexist = True - highestlowtimestamp = -1 - while newerfilesmayexist: - newerfilesmayexist = False - lowesttimestamp = int(time.time()) - lowesttimestampindex = -1 - for moduleindex in range(len(modules)): - if highestlowtimestamp < filetimestamps[filename][moduleindex] < lowesttimestamp: - lowesttimestamp = filetimestamps[filename][moduleindex] - lowesttimestampindex = moduleindex - if lowesttimestampindex != -1: - newerfilesmayexist = True - highestlowtimestamp = lowesttimestamp - originaldata = modules[lowesttimestampindex].read_file(filename) - if originaldata is not None: - originalhash = hashlib.sha1(originaldata).hexdigest() - for moduleindex in range(len(modules)): - if moduleindex != lowesttimestampindex and hashlib.sha1(modules[moduleindex].read_file(filename)).hexdigest() == originalhash: - filetimestamps[filename][moduleindex] = lowesttimestamp - - highesttimestamp = -1 - highesttimestampindex = -1 - for moduleindex in range(len(modules)): - if filetimestamps[filename][moduleindex] > highesttimestamp: - highesttimestamp = filetimestamps[filename][moduleindex] - highesttimestampindex = moduleindex - highesttimestampdata = modules[highesttimestampindex].read_file(filename) - if highesttimestampdata is not None: - for moduleindex in range(len(modules)): - if moduleindex != highesttimestampindex and filetimestamps[filename][moduleindex] < highesttimestamp: - modules[moduleindex].write_file(filename, highesttimestampdata) diff --git a/steamcloud.py b/steamcloud.py index 22316a6..a16af78 100644 --- a/steamcloud.py +++ b/steamcloud.py @@ -2,23 +2,18 @@ import platform import ctypes -def set_id(appid): - os.environ['SteamAppId'] = appid - -def set_folder(folder): - global steamfolder - steamfolder = folder - -def will_work(): +def init(): try: system = platform.system() + bits = platform.architecture()[0][:2] + global steamapi if system == 'Windows': - steamapi = ctypes.CDLL('bin_win/CSteamworks.dll') + steamapi = ctypes.CDLL('bin_win' + bits + '/CSteamworks.dll') elif system == 'Darwin': steamapi = ctypes.CDLL('bin_osx/CSteamworks.dylib') else: - steamapi = ctypes.CDLL('bin_lnx/CSteamworks.so') + steamapi = ctypes.CDLL('bin_lnx' + bits + '/CSteamworks.so') global steamapi_init steamapi_init = steamapi.InitSafe @@ -37,17 +32,34 @@ def will_work(): steamapi_file_write = steamapi.ISteamRemoteStorage_FileWrite global steamapi_file_read steamapi_file_read = steamapi.ISteamRemoteStorage_FileRead + global steamapi_shutdown + steamapi_shutdown = steamapi.Shutdown + + global steamfolder + steamfolder = None - return steamapi_init() + return True except: return False + +def set_id(appid): + os.environ['SteamAppId'] = appid + +def set_folder(folder): + global steamfolder + steamfolder = folder + +def will_work(): + return steamapi_init() + def get_file_names(): filenames = [] + for fileindex in range(steamapi_get_file_count()): filename = steamapi_get_file_name_size(fileindex, None) - if 'steamfolder' in globals(): + if steamfolder is not None: if filename.startswith(steamfolder + '/'): filenames.append(filename[len(steamfolder) + 1:]) else: @@ -55,28 +67,21 @@ def get_file_names(): return filenames def get_file_timestamp(filename): - if 'steamfolder' in globals(): - return steamapi_get_file_timestamp(steamfolder + '/' + filename) - else: - return steamapi_get_file_timestamp(filename) + return steamapi_get_file_timestamp(('' if steamfolder is None else steamfolder + '/') + filename) def read_file(filename): - if 'steamfolder' in globals(): - size = steamapi_get_file_size(steamfolder + '/' + filename) - else: - size = steamapi_get_file_size(filename) + size = steamapi_get_file_size(('' if steamfolder is None else steamfolder + '/') + filename) buffer = ctypes.create_string_buffer(size) - if 'steamfolder' in globals(): - steamapi_file_read(steamfolder + '/' + filename, buffer, size) - else: - steamapi_file_read(filename, buffer, size) + steamapi_file_read(('' if steamfolder is None else steamfolder + '/') + filename, buffer, size) return buffer.value def write_file(filename, data): size = len(data) buffer = ctypes.create_string_buffer(size) buffer.value = data - if 'steamfolder' in globals(): - steamapi_file_write(steamfolder + '/' + filename, buffer, size) - else: - steamapi_file_write(filename, buffer, size) + steamapi_file_write(('' if steamfolder is None else steamfolder + '/') + filename, buffer, size) + +def shutdown(): + global steamfolder + steamfolder = None + steamapi_shutdown()