You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
15 KiB
460 lines
15 KiB
import json
|
|
import os
|
|
import shutil
|
|
import tarfile
|
|
import time
|
|
import traceback
|
|
import zipfile
|
|
from datetime import datetime
|
|
from threading import RLock
|
|
|
|
from couchpotato.api import addApiView
|
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
|
from couchpotato.core.helpers.encoding import sp
|
|
from couchpotato.core.helpers.variable import removePyc
|
|
from couchpotato.core.logger import CPLog
|
|
from couchpotato.core.plugins.base import Plugin
|
|
from couchpotato.environment import Env
|
|
from dateutil.parser import parse
|
|
from git.repository import LocalRepository
|
|
import version
|
|
from six.moves import filter
|
|
|
|
|
|
log = CPLog(__name__)
|
|
|
|
|
|
class Updater(Plugin):
|
|
|
|
available_notified = False
|
|
_lock = RLock()
|
|
|
|
def __init__(self):
|
|
|
|
if Env.get('desktop'):
|
|
self.updater = DesktopUpdater()
|
|
elif os.path.isdir(os.path.join(Env.get('app_dir'), '.git')):
|
|
self.updater = GitUpdater(self.conf('git_command', default = 'git'))
|
|
else:
|
|
self.updater = SourceUpdater()
|
|
|
|
addEvent('app.load', self.logVersion, priority = 10000)
|
|
addEvent('app.load', self.setCrons)
|
|
addEvent('updater.info', self.info)
|
|
|
|
addApiView('updater.info', self.info, docs = {
|
|
'desc': 'Get updater information',
|
|
'return': {
|
|
'type': 'object',
|
|
'example': """{
|
|
'last_check': "last checked for update",
|
|
'update_version': "available update version or empty",
|
|
'version': current_cp_version
|
|
}"""}
|
|
})
|
|
addApiView('updater.update', self.doUpdateView)
|
|
addApiView('updater.check', self.checkView, docs = {
|
|
'desc': 'Check for available update',
|
|
'return': {'type': 'see updater.info'}
|
|
})
|
|
|
|
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
|
|
|
def logVersion(self):
|
|
info = self.info()
|
|
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
|
|
|
|
def setCrons(self):
|
|
|
|
fireEvent('schedule.remove', 'updater.check', single = True)
|
|
if self.isEnabled():
|
|
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
|
self.autoUpdate() # Check after enabling
|
|
|
|
def autoUpdate(self):
|
|
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
|
if self.updater.doUpdate():
|
|
|
|
# Notify before restarting
|
|
try:
|
|
if self.conf('notification'):
|
|
info = self.updater.info()
|
|
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
|
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
|
except:
|
|
log.error('Failed notifying for update: %s', traceback.format_exc())
|
|
|
|
fireEventAsync('app.restart')
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
def check(self, force = False):
|
|
if not force and self.isDisabled():
|
|
return
|
|
|
|
if self.updater.check():
|
|
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
|
info = self.updater.info()
|
|
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
|
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
|
self.available_notified = True
|
|
return True
|
|
|
|
return False
|
|
|
|
def info(self, **kwargs):
|
|
self._lock.acquire()
|
|
|
|
info = {}
|
|
try:
|
|
info = self.updater.info()
|
|
except:
|
|
log.error('Failed getting updater info: %s', traceback.format_exc())
|
|
|
|
self._lock.release()
|
|
|
|
return info
|
|
|
|
def checkView(self, **kwargs):
|
|
return {
|
|
'update_available': self.check(force = True),
|
|
'info': self.updater.info()
|
|
}
|
|
|
|
def doUpdateView(self, **kwargs):
|
|
|
|
self.check()
|
|
if not self.updater.update_version:
|
|
log.error('Trying to update when no update is available.')
|
|
success = False
|
|
else:
|
|
success = self.updater.doUpdate()
|
|
if success:
|
|
fireEventAsync('app.restart')
|
|
|
|
# Assume the updater handles things
|
|
if not success:
|
|
success = True
|
|
|
|
return {
|
|
'success': success
|
|
}
|
|
|
|
def doShutdown(self, *args, **kwargs):
|
|
if not Env.get('dev') and not Env.get('desktop'):
|
|
removePyc(Env.get('app_dir'), show_logs = False)
|
|
|
|
return super(Updater, self).doShutdown(*args, **kwargs)
|
|
|
|
|
|
class BaseUpdater(Plugin):
|
|
|
|
repo_user = 'RuudBurger'
|
|
repo_name = 'CouchPotatoServer'
|
|
branch = version.BRANCH
|
|
|
|
version = None
|
|
update_failed = False
|
|
update_version = None
|
|
last_check = 0
|
|
auto_register_static = False
|
|
|
|
def doUpdate(self):
|
|
pass
|
|
|
|
def info(self):
|
|
|
|
current_version = self.getVersion()
|
|
|
|
return {
|
|
'last_check': self.last_check,
|
|
'update_version': self.update_version,
|
|
'version': current_version,
|
|
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
|
|
'branch': current_version.get('branch', self.branch),
|
|
}
|
|
|
|
def getVersion(self):
|
|
pass
|
|
|
|
def check(self):
|
|
pass
|
|
|
|
|
|
class GitUpdater(BaseUpdater):
|
|
|
|
def __init__(self, git_command):
|
|
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
|
|
|
|
def doUpdate(self):
|
|
|
|
try:
|
|
log.info('Updating to latest version')
|
|
self.repo.pull()
|
|
|
|
return True
|
|
except:
|
|
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
|
|
|
self.update_failed = True
|
|
|
|
return False
|
|
|
|
def getVersion(self):
|
|
|
|
if not self.version:
|
|
try:
|
|
output = self.repo.getHead() # Yes, please
|
|
log.debug('Git version output: %s', output.hash)
|
|
self.version = {
|
|
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
|
'hash': output.hash[:8],
|
|
'date': output.getDate(),
|
|
'type': 'git',
|
|
'branch': self.repo.getCurrentBranch().name
|
|
}
|
|
except Exception as e:
|
|
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
|
return 'No GIT'
|
|
|
|
return self.version
|
|
|
|
def check(self):
|
|
|
|
if self.update_version:
|
|
return True
|
|
|
|
log.info('Checking for new version on github for %s', self.repo_name)
|
|
if not Env.get('dev'):
|
|
self.repo.fetch()
|
|
|
|
current_branch = self.repo.getCurrentBranch().name
|
|
|
|
for branch in self.repo.getRemoteByName('origin').getBranches():
|
|
if current_branch == branch.name:
|
|
|
|
local = self.repo.getHead()
|
|
remote = branch.getHead()
|
|
|
|
log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
|
|
|
|
if local.getDate() < remote.getDate():
|
|
self.update_version = {
|
|
'hash': remote.hash[:8],
|
|
'date': remote.getDate(),
|
|
}
|
|
return True
|
|
|
|
self.last_check = time.time()
|
|
return False
|
|
|
|
|
|
class SourceUpdater(BaseUpdater):
|
|
|
|
def __init__(self):
|
|
|
|
# Create version file in cache
|
|
self.version_file = os.path.join(Env.get('cache_dir'), 'version')
|
|
if not os.path.isfile(self.version_file):
|
|
self.createFile(self.version_file, json.dumps(self.latestCommit()))
|
|
|
|
def doUpdate(self):
|
|
|
|
try:
|
|
download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
|
|
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
|
|
|
|
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
|
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
|
|
|
|
# Cleanup leftover from last time
|
|
if os.path.isdir(extracted_path):
|
|
self.removeDir(extracted_path)
|
|
self.makeDir(extracted_path)
|
|
|
|
# Extract
|
|
if download_data.get('type') == 'zip':
|
|
zip_file = zipfile.ZipFile(destination)
|
|
zip_file.extractall(extracted_path)
|
|
zip_file.close()
|
|
else:
|
|
tar = tarfile.open(destination)
|
|
tar.extractall(path = extracted_path)
|
|
tar.close()
|
|
|
|
os.remove(destination)
|
|
|
|
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
|
self.removeDir(extracted_path)
|
|
|
|
# Write update version to file
|
|
self.createFile(self.version_file, json.dumps(self.update_version))
|
|
|
|
return True
|
|
except:
|
|
log.error('Failed updating: %s', traceback.format_exc())
|
|
|
|
self.update_failed = True
|
|
return False
|
|
|
|
def replaceWith(self, path):
|
|
path = sp(path)
|
|
app_dir = Env.get('app_dir')
|
|
data_dir = Env.get('data_dir')
|
|
|
|
# Get list of files we want to overwrite
|
|
removePyc(app_dir)
|
|
existing_files = []
|
|
for root, subfiles, filenames in os.walk(app_dir):
|
|
for filename in filenames:
|
|
existing_files.append(os.path.join(root, filename))
|
|
|
|
for root, subfiles, filenames in os.walk(path):
|
|
for filename in filenames:
|
|
fromfile = os.path.join(root, filename)
|
|
tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, ''))
|
|
|
|
if not Env.get('dev'):
|
|
try:
|
|
if os.path.isfile(tofile):
|
|
os.remove(tofile)
|
|
|
|
dirname = os.path.dirname(tofile)
|
|
if not os.path.isdir(dirname):
|
|
self.makeDir(dirname)
|
|
|
|
shutil.move(fromfile, tofile)
|
|
try:
|
|
existing_files.remove(tofile)
|
|
except ValueError:
|
|
pass
|
|
except:
|
|
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
|
return False
|
|
|
|
for still_exists in existing_files:
|
|
|
|
if data_dir in still_exists:
|
|
continue
|
|
|
|
try:
|
|
os.remove(still_exists)
|
|
except:
|
|
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
|
|
|
return True
|
|
|
|
def removeDir(self, path):
|
|
try:
|
|
if os.path.isdir(path):
|
|
shutil.rmtree(path)
|
|
except OSError as inst:
|
|
os.chmod(inst.filename, 0o777)
|
|
self.removeDir(path)
|
|
|
|
def getVersion(self):
|
|
|
|
if not self.version:
|
|
try:
|
|
f = open(self.version_file, 'r')
|
|
output = json.loads(f.read())
|
|
f.close()
|
|
|
|
log.debug('Source version output: %s', output)
|
|
self.version = output
|
|
self.version['type'] = 'source'
|
|
self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0)))
|
|
except Exception as e:
|
|
log.error('Failed using source updater. %s', e)
|
|
return {}
|
|
|
|
return self.version
|
|
|
|
def check(self):
|
|
|
|
current_version = self.getVersion()
|
|
|
|
try:
|
|
latest = self.latestCommit()
|
|
|
|
if latest.get('hash') != current_version.get('hash') and latest.get('date') >= current_version.get('date'):
|
|
self.update_version = latest
|
|
|
|
self.last_check = time.time()
|
|
except:
|
|
log.error('Failed updating via source: %s', traceback.format_exc())
|
|
|
|
return self.update_version is not None
|
|
|
|
def latestCommit(self):
|
|
try:
|
|
url = 'https://api.github.com/repos/%s/%s/commits?per_page=1&sha=%s' % (self.repo_user, self.repo_name, self.branch)
|
|
data = self.getCache('github.commit', url = url)
|
|
commit = json.loads(data)[0]
|
|
|
|
return {
|
|
'hash': commit['sha'],
|
|
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
|
}
|
|
except:
|
|
log.error('Failed getting latest request from github: %s', traceback.format_exc())
|
|
|
|
return {}
|
|
|
|
|
|
class DesktopUpdater(BaseUpdater):
|
|
|
|
def __init__(self):
|
|
self.desktop = Env.get('desktop')
|
|
|
|
def doUpdate(self):
|
|
try:
|
|
def do_restart(e):
|
|
if e['status'] == 'done':
|
|
fireEventAsync('app.restart')
|
|
elif e['status'] == 'error':
|
|
log.error('Failed updating desktop: %s', e['exception'])
|
|
self.update_failed = True
|
|
|
|
self.desktop._esky.auto_update(callback = do_restart)
|
|
return
|
|
except:
|
|
self.update_failed = True
|
|
|
|
return False
|
|
|
|
def info(self):
|
|
return {
|
|
'last_check': self.last_check,
|
|
'update_version': self.update_version,
|
|
'version': self.getVersion(),
|
|
'branch': self.branch,
|
|
}
|
|
|
|
def check(self):
|
|
current_version = self.getVersion()
|
|
try:
|
|
latest = self.desktop._esky.find_update()
|
|
|
|
if latest and latest != current_version.get('hash'):
|
|
self.update_version = {
|
|
'hash': latest,
|
|
'date': None,
|
|
'changelog': self.desktop._changelogURL,
|
|
}
|
|
|
|
self.last_check = time.time()
|
|
except:
|
|
log.error('Failed updating desktop: %s', traceback.format_exc())
|
|
|
|
return self.update_version is not None
|
|
|
|
def getVersion(self):
|
|
return {
|
|
'repr': 'desktop: %s' % self.desktop._esky.active_version,
|
|
'hash': self.desktop._esky.active_version,
|
|
'date': None,
|
|
'type': 'desktop',
|
|
}
|
|
|