from couchpotato . api import addApiView
from couchpotato . core . event import addEvent , fireEvent , fireEventAsync
from couchpotato . core . helpers . encoding import ss
from couchpotato . core . helpers . request import jsonified
from couchpotato . core . logger import CPLog
from couchpotato . core . plugins . base import Plugin
from couchpotato . environment import Env
from datetime import datetime
from dateutil . parser import parse
from git . repository import LocalRepository
import json
import os
import shutil
import tarfile
import time
import traceback
log = CPLog ( __name__ )
class Updater ( Plugin ) :
available_notified = False
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 ( )
fireEvent ( ' schedule.interval ' , ' updater.check ' , self . autoUpdate , hours = 6 )
addEvent ( ' app.load ' , self . autoUpdate )
addEvent ( ' updater.info ' , self . info )
addApiView ( ' updater.info ' , self . getInfo , 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 ' }
} )
def autoUpdate ( self ) :
if 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 ) :
if self . isDisabled ( ) :
return
if self . updater . check ( ) :
if not self . available_notified and self . conf ( ' notification ' ) and not self . conf ( ' automatic ' ) :
fireEvent ( ' updater.available ' , message = ' A new update is available ' , data = self . updater . info ( ) )
self . available_notified = True
return True
return False
def info ( self ) :
return self . updater . info ( )
def getInfo ( self ) :
return jsonified ( self . updater . info ( ) )
def checkView ( self ) :
return jsonified ( {
' update_available ' : self . check ( ) ,
' info ' : self . updater . info ( )
} )
def doUpdateView ( self ) :
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 ( )
return jsonified ( {
' success ' : success
} )
class BaseUpdater ( Plugin ) :
repo_user = ' RuudBurger '
repo_name = ' CouchPotatoServer '
branch = ' master '
version = None
update_failed = False
update_version = None
last_check = 0
def doUpdate ( self ) :
pass
def getInfo ( self ) :
return jsonified ( self . info ( ) )
def info ( self ) :
return {
' last_check ' : self . last_check ,
' update_version ' : self . update_version ,
' version ' : self . getVersion ( ) ,
' repo_name ' : ' %s / %s ' % ( self . repo_user , self . repo_name ) ,
' branch ' : self . branch ,
}
def check ( self ) :
pass
def deletePyc ( self , only_excess = True ) :
for root , dirs , files in os . walk ( ss ( Env . get ( ' app_dir ' ) ) ) :
pyc_files = filter ( lambda filename : filename . endswith ( ' .pyc ' ) , files )
py_files = set ( filter ( lambda filename : filename . endswith ( ' .py ' ) , files ) )
excess_pyc_files = filter ( lambda pyc_filename : pyc_filename [ : - 1 ] not in py_files , pyc_files ) if only_excess else pyc_files
for excess_pyc_file in excess_pyc_files :
full_path = os . path . join ( root , excess_pyc_file )
log . debug ( ' Removing old PYC file: %s ' , full_path )
try :
os . remove ( full_path )
except :
log . error ( ' Couldn \' t remove %s : %s ' , ( full_path , traceback . format_exc ( ) ) )
for dir_name in dirs :
full_path = os . path . join ( root , dir_name )
if len ( os . listdir ( full_path ) ) == 0 :
try :
os . rmdir ( full_path )
except :
log . error ( ' Couldn \' t remove empty directory %s : %s ' , ( full_path , traceback . format_exc ( ) ) )
class GitUpdater ( BaseUpdater ) :
def __init__ ( self , git_command ) :
self . repo = LocalRepository ( Env . get ( ' app_dir ' ) , command = git_command )
def doUpdate ( self ) :
try :
log . debug ( ' Stashing local changes ' )
self . repo . saveStash ( )
log . info ( ' Updating to latest version ' )
self . repo . pull ( )
# Delete leftover .pyc files
self . deletePyc ( )
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 = {
' hash ' : output . hash [ : 8 ] ,
' date ' : output . getDate ( ) ,
' type ' : ' git ' ,
}
except Exception , 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 . info ( ' 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 :
url = ' https://github.com/ %s / %s /tarball/ %s ' % ( self . repo_user , self . repo_name , self . branch )
destination = os . path . join ( Env . get ( ' cache_dir ' ) , self . update_version . get ( ' hash ' ) + ' .tar.gz ' )
extracted_path = os . path . join ( Env . get ( ' cache_dir ' ) , ' temp_updater ' )
destination = fireEvent ( ' file.download ' , url = 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
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 ) :
app_dir = ss ( Env . get ( ' app_dir ' ) )
# Get list of files we want to overwrite
self . deletePyc ( )
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 )
os . rename ( fromfile , tofile )
try :
existing_files . remove ( tofile )
except ValueError :
pass
except :
log . error ( ' Failed overwriting file " %s " : %s ' , ( tofile , traceback . format_exc ( ) ) )
return False
if Env . get ( ' app_dir ' ) not in Env . get ( ' data_dir ' ) :
for still_exists in existing_files :
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 , inst :
os . chmod ( inst . filename , 0777 )
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 '
except Exception , 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 ) :
version = None
update_failed = False
update_version = None
last_check = 0
def __init__ ( self ) :
self . desktop = Env . get ( ' desktop ' )
def doUpdate ( self ) :
try :
def do_restart ( e ) :
if e [ ' status ' ] == ' done ' :
fireEventAsync ( ' app.restart ' )
else :
log . error ( ' Failed updating desktop: %s ' , e [ ' exception ' ] )
self . update_failed = True
self . desktop . _esky . auto_update ( callback = do_restart )
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 ' : ' desktop_build ' ,
}
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 {
' hash ' : self . desktop . _esky . active_version ,
' date ' : None ,
' type ' : ' desktop ' ,
}