import os import logging import shutil import sys import json from urllib2 import urlopen from StringIO import StringIO from twisted.internet import defer from twisted.internet.task import LoopingCall from lbrynet.conf import settings from lbrynet.lbrynet_daemon.Resources import NoCacheStaticFile from lbrynet import __version__ as lbrynet_version from lbryum.version import LBRYUM_VERSION as lbryum_version from zipfile import ZipFile from appdirs import user_data_dir if sys.platform != "darwin": log_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") else: log_dir = user_data_dir("LBRY") if not os.path.isdir(log_dir): os.mkdir(log_dir) lbrynet_log = os.path.join(log_dir, settings.LOG_FILE_NAME) log = logging.getLogger(__name__) class UIManager(object): def __init__(self, root): if sys.platform != "darwin": self.data_dir = os.path.join(os.path.expanduser("~"), '.lbrynet') else: self.data_dir = user_data_dir("LBRY") self.ui_root = os.path.join(self.data_dir, "lbry-ui") self.active_dir = os.path.join(self.ui_root, "active") self.update_dir = os.path.join(self.ui_root, "update") if not os.path.isdir(self.data_dir): os.mkdir(self.data_dir) if not os.path.isdir(self.ui_root): os.mkdir(self.ui_root) if not os.path.isdir(self.active_dir): os.mkdir(self.active_dir) if not os.path.isdir(self.update_dir): os.mkdir(self.update_dir) self.config = os.path.join(self.ui_root, "active.json") self.update_requires = os.path.join(self.update_dir, "requirements.txt") self.requirements = {} self.check_requirements = True self.ui_dir = self.active_dir self.git_version = None self.root = root self.branch = None self.update_checker = LoopingCall(self.setup) if not os.path.isfile(os.path.join(self.config)): self.loaded_git_version = None self.loaded_branch = None self.loaded_requirements = None else: try: f = open(self.config, "r") loaded_ui = json.loads(f.read()) f.close() self.loaded_git_version = loaded_ui['commit'] self.loaded_branch = loaded_ui['branch'] self.loaded_requirements = loaded_ui['requirements'] except: self.loaded_git_version = None self.loaded_branch = None self.loaded_requirements = None def setup(self, branch=None, check_requirements=None, user_specified=None): local_ui_path = settings.local_ui_path or user_specified self.branch = settings.ui_branch or branch self.check_requirements = settings.check_ui_requirements or check_requirements if local_ui_path: if os.path.isdir(local_ui_path): log.info("Checking user specified UI directory: " + str(local_ui_path)) self.branch = "user-specified" self.loaded_git_version = "user-specified" d = self.migrate_ui(source=local_ui_path) d.addCallback(lambda _: self._load_ui()) return d else: log.info("User specified UI directory doesn't exist, using " + self.branch) elif self.loaded_branch == "user-specified": log.info("Loading user provided UI") d = self._load_ui() return d else: log.info("Checking for updates for UI branch: " + self.branch) self._git_url = "https://s3.amazonaws.com/lbry-ui/{}/data.json".format(self.branch) self._dist_url = "https://s3.amazonaws.com/lbry-ui/{}/dist.zip".format(self.branch) d = self._up_to_date() d.addCallback(lambda r: self._download_ui() if not r else self._load_ui()) return d def _up_to_date(self): def _get_git_info(): try: response = urlopen(self._git_url) data = json.loads(response.read()) return defer.succeed(data['sha']) except Exception: return defer.fail() def _set_git(version): self.git_version = version.replace('\n', '') if self.git_version == self.loaded_git_version: log.info("UI is up to date") return defer.succeed(True) else: log.info("UI updates available, checking if installation meets requirements") return defer.succeed(False) def _use_existing(): log.info("Failed to check for new ui version, trying to use cached ui") return defer.succeed(True) d = _get_git_info() d.addCallbacks(_set_git, lambda _: _use_existing) return d def migrate_ui(self, source=None): if not source: requires_file = self.update_requires source_dir = self.update_dir delete_source = True else: requires_file = os.path.join(source, "requirements.txt") source_dir = source delete_source = False def _skip_requirements(): log.info("Skipping ui requirement check") return defer.succeed(True) def _check_requirements(): if not os.path.isfile(requires_file): log.info("No requirements.txt file, rejecting request to migrate this UI") return defer.succeed(False) f = open(requires_file, "r") for requirement in [line for line in f.read().split('\n') if line]: t = requirement.split('=') if len(t) == 3: self.requirements[t[0]] = {'version': t[1], 'operator': '=='} elif t[0][-1] == ">": self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '>='} elif t[0][-1] == "<": self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '<='} f.close() passed_requirements = True for r in self.requirements: if r == 'lbrynet': c = lbrynet_version elif r == 'lbryum': c = lbryum_version else: c = None if c: if self.requirements[r]['operator'] == '==': if not self.requirements[r]['version'] == c: passed_requirements = False log.info("Local version %s of %s does not meet UI requirement for version %s" % ( c, r, self.requirements[r]['version'])) else: log.info("Local version of %s meets ui requirement" % r) if self.requirements[r]['operator'] == '>=': if not self.requirements[r]['version'] <= c: passed_requirements = False log.info("Local version %s of %s does not meet UI requirement for version %s" % ( c, r, self.requirements[r]['version'])) else: log.info("Local version of %s meets ui requirement" % r) if self.requirements[r]['operator'] == '<=': if not self.requirements[r]['version'] >= c: passed_requirements = False log.info("Local version %s of %s does not meet UI requirement for version %s" % ( c, r, self.requirements[r]['version'])) else: log.info("Local version of %s meets ui requirement" % r) return defer.succeed(passed_requirements) def _disp_failure(): log.info("Failed to satisfy requirements for branch '%s', update was not loaded" % self.branch) return defer.succeed(False) def _do_migrate(): if os.path.isdir(self.active_dir): shutil.rmtree(self.active_dir) shutil.copytree(source_dir, self.active_dir) if delete_source: shutil.rmtree(source_dir) log.info("Loaded UI update") f = open(self.config, "w") loaded_ui = {'commit': self.git_version, 'branch': self.branch, 'requirements': self.requirements} f.write(json.dumps(loaded_ui)) f.close() self.loaded_git_version = loaded_ui['commit'] self.loaded_branch = loaded_ui['branch'] self.loaded_requirements = loaded_ui['requirements'] return defer.succeed(True) d = _check_requirements() if self.check_requirements else _skip_requirements() d.addCallback(lambda r: _do_migrate() if r else _disp_failure()) return d def _download_ui(self): def _delete_update_dir(): if os.path.isdir(self.update_dir): shutil.rmtree(self.update_dir) return defer.succeed(None) def _dl_ui(): url = urlopen(self._dist_url) z = ZipFile(StringIO(url.read())) names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i] z.extractall(self.update_dir, members=names) log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", "")) return self.branch d = _delete_update_dir() d.addCallback(lambda _: _dl_ui()) d.addCallback(lambda _: self.migrate_ui()) d.addCallback(lambda _: self._load_ui()) return d def _load_ui(self): for d in [i[0] for i in os.walk(self.active_dir) if os.path.dirname(i[0]) == self.active_dir]: self.root.putChild(os.path.basename(d), NoCacheStaticFile(d)) return defer.succeed(True)