diff --git a/README.md b/README.md index 9663357..053be07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Backup Addon +__Kodi Version Compatibility:__ Kodi 17.x (Krypton) and greater + ## About I've had to recover my database, thumbnails, and source configuration enough times that I just wanted a quick easy way to back them up. That is what this addon is meant to do. @@ -17,6 +19,11 @@ For more specific information please check out the [wiki on Github](https://gith * [FAQ](https://github.com/robweber/xbmcbackup/wiki/FAQ) +## Attributions + +Icon files from Open Iconic — www.useiconic.com/open + + diff --git a/addon.xml b/addon.xml index 9c4cdf7..1f44f0a 100644 --- a/addon.xml +++ b/addon.xml @@ -1,9 +1,9 @@  + name="Backup" version="1.5.0~beta3" provider-name="robweber"> - + @@ -89,7 +89,11 @@ https://github.com/robweber/xbmcbackup - resources/media/icon.png + resources/images/icon.png + resources/images/screenshot1.png + resources/images/screenshot2.png + resources/images/screenshot3.png + resources/images/screenshot4.png Version 1.1.4 - added file chunk support for dropbox uploads diff --git a/authorize_cloud.py b/authorize_cloud.py deleted file mode 100644 index d8d5b8f..0000000 --- a/authorize_cloud.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -import urlparse -import xbmcgui -import resources.lib.utils as utils -from resources.lib.authorizers import DropboxAuthorizer,GoogleDriveAuthorizer - -def get_params(): - param = {} - try: - for i in sys.argv: - args = i - if(args.startswith('?')): - args = args[1:] - param.update(dict(urlparse.parse_qsl(args))) - except: - pass - return param - -params = get_params() - -#drobpox -if(params['type'] == 'dropbox'): - authorizer = DropboxAuthorizer() - - if(authorizer.authorize()): - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30027) + ' ' + utils.getString(30106)) - else: - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30107) + ' ' + utils.getString(30027)) - -#google drive -elif(params['type'] == 'google_drive'): - authorizer = GoogleDriveAuthorizer() - - if(authorizer.authorize()): - xbmcgui.Dialog().ok("Backup",utils.getString(30098) + ' ' + utils.getString(30106)) - else: - xbmcgui.Dialog().ok("Backup",utils.getString(30107) + ' ' + utils.getString(30098)) diff --git a/default.py b/default.py index 7a3f43d..04cfe04 100644 --- a/default.py +++ b/default.py @@ -1,77 +1,91 @@ -import urlparse -import xbmcgui -import resources.lib.utils as utils -from resources.lib.backup import XbmcBackup - -def get_params(): - param = {} - - if(len(sys.argv) > 1): - for i in sys.argv: - args = i - if(args.startswith('?')): - args = args[1:] - param.update(dict(urlparse.parse_qsl(args))) - - return param - -#the program mode -mode = -1 -params = get_params() - - -if("mode" in params): - if(params['mode'] == 'backup'): - mode = 0 - elif(params['mode'] == 'restore'): - mode = 1 - -#if mode wasn't passed in as arg, get from user -if(mode == -1): - #figure out if this is a backup or a restore from the user - mode = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30023),[utils.getString(30016),utils.getString(30017),utils.getString(30099)]) - -#check if program should be run -if(mode != -1): - #run the profile backup - backup = XbmcBackup() - - if(mode == 2): - #open the settings dialog - utils.openSettings() - - elif(backup.remoteConfigured()): - - if(mode == backup.Restore): - #get list of valid restore points - restorePoints = backup.listBackups() - pointNames = [] - folderNames = [] - - for aDir in restorePoints: - pointNames.append(aDir[1]) - folderNames.append(aDir[0]) - - selectedRestore = -1 - - if("archive" in params): - #check that the user give archive exists - if(params['archive'] in folderNames): - #set the index - selectedRestore = folderNames.index(params['archive']) - utils.log(str(selectedRestore) + " : " + params['archive']) - else: - utils.showNotification(utils.getString(30045)) - utils.log(params['archive'] + ' is not a valid restore point') - else: - #allow user to select the backup to restore from - selectedRestore = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30021),pointNames) - - if(selectedRestore != -1): - backup.selectRestore(restorePoints[selectedRestore][0]) - - backup.run(mode) - else: - #can't go any further - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045)) - utils.openSettings() +import sys, urlparse +import xbmc, xbmcgui +import resources.lib.utils as utils +from resources.lib.backup import XbmcBackup + +def get_params(): + param = {} + + if(len(sys.argv) > 1): + for i in sys.argv: + args = i + if(args.startswith('?')): + args = args[1:] + param.update(dict(urlparse.parse_qsl(args))) + + return param + +#the program mode +mode = -1 +params = get_params() + + +if("mode" in params): + if(params['mode'] == 'backup'): + mode = 0 + elif(params['mode'] == 'restore'): + mode = 1 + +#if mode wasn't passed in as arg, get from user +if(mode == -1): + #by default, Backup,Restore,Open Settings + options = [utils.getString(30016),utils.getString(30017),utils.getString(30099)] + + #find out if we're using the advanced editor + if(int(utils.getSetting('backup_selection_type')) == 1): + options.append(utils.getString(30125)) + + #figure out if this is a backup or a restore from the user + mode = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30023),options) + +#check if program should be run +if(mode != -1): + #run the profile backup + backup = XbmcBackup() + + if(mode == 2): + #open the settings dialog + utils.openSettings() + elif(mode == 3 and int(utils.getSetting('backup_selection_type')) == 1): + #open the advanced editor + xbmc.executebuiltin('RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_editor)') + elif(backup.remoteConfigured()): + + if(mode == backup.Restore): + #get list of valid restore points + restorePoints = backup.listBackups() + pointNames = [] + folderNames = [] + + for aDir in restorePoints: + pointNames.append(aDir[1]) + folderNames.append(aDir[0]) + + selectedRestore = -1 + + if("archive" in params): + #check that the user give archive exists + if(params['archive'] in folderNames): + #set the index + selectedRestore = folderNames.index(params['archive']) + utils.log(str(selectedRestore) + " : " + params['archive']) + else: + utils.showNotification(utils.getString(30045)) + utils.log(params['archive'] + ' is not a valid restore point') + else: + #allow user to select the backup to restore from + selectedRestore = xbmcgui.Dialog().select(utils.getString(30010) + " - " + utils.getString(30021),pointNames) + + if(selectedRestore != -1): + backup.selectRestore(restorePoints[selectedRestore][0]) + + if('sets' in params): + backup.restore(selectedSets=params['sets'].split('|')) + else: + backup.restore() + else: + backup.backup() + else: + #can't go any further + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045)) + utils.openSettings() diff --git a/launcher.py b/launcher.py new file mode 100644 index 0000000..0b0fa1b --- /dev/null +++ b/launcher.py @@ -0,0 +1,64 @@ +import sys +import urlparse +import xbmc +import xbmcgui +import xbmcvfs +import resources.lib.utils as utils +from resources.lib.authorizers import DropboxAuthorizer,GoogleDriveAuthorizer +from resources.lib.advanced_editor import AdvancedBackupEditor + + +#launcher for various helpful functions found in the settings.xml area + +def authorize_cloud(cloudProvider): + #drobpox + if(cloudProvider == 'dropbox'): + authorizer = DropboxAuthorizer() + + if(authorizer.authorize()): + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30027) + ' ' + utils.getString(30106)) + else: + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30107) + ' ' + utils.getString(30027)) + + #google drive + elif(cloudProvider == 'google_drive'): + authorizer = GoogleDriveAuthorizer() + + if(authorizer.authorize()): + xbmcgui.Dialog().ok("Backup",utils.getString(30098) + ' ' + utils.getString(30106)) + else: + xbmcgui.Dialog().ok("Backup",utils.getString(30107) + ' ' + utils.getString(30098)) + +def remove_auth(): + #triggered from settings.xml - asks if user wants to delete OAuth token information + shouldDelete = xbmcgui.Dialog().yesno(utils.getString(30093),utils.getString(30094),utils.getString(30095),autoclose=7000) + + if(shouldDelete): + #delete any of the known token file types + xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt")) #dropbox + xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "google_drive.dat")) #google drive + +def get_params(): + param = {} + try: + for i in sys.argv: + args = i + if(args.startswith('?')): + args = args[1:] + param.update(dict(urlparse.parse_qsl(args))) + except: + pass + return param + +params = get_params() + +if(params['action'] == 'authorize_cloud'): + authorize_cloud(params['provider']) +elif(params['action'] == 'remove_auth'): + remove_auth() +elif(params['action'] == 'advanced_editor'): + editor = AdvancedBackupEditor() + editor.showMainScreen() +elif(params['action'] == 'advanced_copy_config'): + editor = AdvancedBackupEditor() + editor.copySimpleConfig() diff --git a/remove_auth.py b/remove_auth.py deleted file mode 100644 index f9ac08e..0000000 --- a/remove_auth.py +++ /dev/null @@ -1,13 +0,0 @@ -import xbmc -import xbmcgui -import xbmcvfs -import resources.lib.utils as utils - -#triggered from settings.xml - asks if user wants to delete OAuth token information -shouldDelete = xbmcgui.Dialog().yesno(utils.getString(30093),utils.getString(30094),utils.getString(30095),autoclose=7000) - -if(shouldDelete): - #delete any of the known token file types - xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt")) #dropbox - xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "google_drive.dat")) #google drive - diff --git a/resources/data/default_files.json b/resources/data/default_files.json new file mode 100644 index 0000000..fc92ed1 --- /dev/null +++ b/resources/data/default_files.json @@ -0,0 +1,105 @@ +{ + "addons":{ + "root":"special://home/addons/", + "dirs":[ + { + "type":"include", + "path":"special://home/addons/", + "recurse":true + }, + { + "type":"exclude", + "path":"special://home/addons/packages/" + }, + { + "type":"exclude", + "path":"special://home/addons/temp/" + } + ] + }, + "addon_data":{ + "root":"special://home/userdata/addon_data/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/addon_data/", + "recurse":true + } + ] + }, + "database":{ + "root":"special://home/userdata/Database/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/Database/", + "recurse":true + } + ] + }, + "game_saves":{ + "root":"special://home/userdata/Savestates/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/Savestates/", + "recurse":true + } + ] + }, + "playlists":{ + "root":"special://home/userdata/playlists/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/playlists/", + "recurse":true + } + ] + }, + "profiles":{ + "root":"special://home/userdata/profiles/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/profiles/", + "recurse":true + } + ] + }, + "thumbnails":{ + "root":"special://home/userdata/Thumbnails/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/Thumbnails/", + "recurse":true + } + ] + }, + "config":{ + "root":"special://home/userdata/", + "dirs":[ + { + "type":"include", + "path":"special://home/userdata/", + "recurse":false + }, + { + "type":"include", + "path":"special://home/userdata/keymaps/", + "recurse":true + }, + { + "type":"include", + "path":"special://home/userdata/peripheral_data/", + "recurse":true + }, + { + "type":"include", + "path":"special://home/userdata/library/", + "recurse":true + } + ] + } +} \ No newline at end of file diff --git a/resources/images/folder-icon.png b/resources/images/folder-icon.png new file mode 100644 index 0000000..e2f6dbf Binary files /dev/null and b/resources/images/folder-icon.png differ diff --git a/resources/media/icon.png b/resources/images/icon.png similarity index 100% rename from resources/media/icon.png rename to resources/images/icon.png diff --git a/resources/images/plus-icon.png b/resources/images/plus-icon.png new file mode 100644 index 0000000..0a343a1 Binary files /dev/null and b/resources/images/plus-icon.png differ diff --git a/resources/images/screenshot1.PNG b/resources/images/screenshot1.PNG new file mode 100644 index 0000000..268acda Binary files /dev/null and b/resources/images/screenshot1.PNG differ diff --git a/resources/images/screenshot2.PNG b/resources/images/screenshot2.PNG new file mode 100644 index 0000000..08ae592 Binary files /dev/null and b/resources/images/screenshot2.PNG differ diff --git a/resources/images/screenshot3.PNG b/resources/images/screenshot3.PNG new file mode 100644 index 0000000..2861573 Binary files /dev/null and b/resources/images/screenshot3.PNG differ diff --git a/resources/images/screenshot4.PNG b/resources/images/screenshot4.PNG new file mode 100644 index 0000000..7b09988 Binary files /dev/null and b/resources/images/screenshot4.PNG differ diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index d099b9d..2be2b52 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -48,6 +48,14 @@ msgctxt "#30013" msgid "Scheduling" msgstr "Scheduling" +msgctxt "#30014" +msgid "Simple" +msgstr "Simple" + +msgctxt "#30015" +msgid "Advanced" +msgstr "Advanced" + msgctxt "#30016" msgid "Backup" msgstr "Backup" @@ -129,12 +137,12 @@ msgid "Config Files" msgstr "Config Files" msgctxt "#30036" -msgid "Custom Directory 1" -msgstr "Custom Directory 1" +msgid "Disclaimer" +msgstr "Disclaimer" msgctxt "#30037" -msgid "Custom Directory 2" -msgstr "Custom Directory 2" +msgid "Canceling this menu will close and save changes" +msgstr "Canceling this menu will close and save changes" msgctxt "#30038" msgid "Advanced Settings Detected" @@ -420,3 +428,131 @@ msgstr "Visit https://console.developers.google.com/" msgctxt "#30109" msgid "Run on startup if missed" msgstr "Run on startup if missed" + +msgctxt "#30110" +msgid "Set Name" +msgstr "" + +msgctxt "#30111" +msgid "Root folder selection" +msgstr "" + +msgctxt "#30112" +msgid "Browse Folder" +msgstr "" + +msgctxt "#30113" +msgid "Enter Own" +msgstr "" + +msgctxt "#30114" +msgid "starts in Kodi home" +msgstr "" + +msgctxt "#30115" +msgid "enter path to start there" +msgstr "" + +msgctxt "#30116" +msgid "Enter root path" +msgstr "" + +msgctxt "#30117" +msgid "Path Error" +msgstr "" + +msgctxt "#30118" +msgid "Path does not exist" +msgstr "" + +msgctxt "#30119" +msgid "Select root" +msgstr "" + +msgctxt "#30120" +msgid "Add Exclude Folder" +msgstr "" + +msgctxt "#30121" +msgid "Root Folder" +msgstr "" + +msgctxt "#30122" +msgid "Edit" +msgstr "" + +msgctxt "#30123" +msgid "Delete" +msgstr "" + +msgctxt "#30124" +msgid "Choose Action" +msgstr "" + +msgctxt "#30125" +msgid "Advanced Editor" +msgstr "" + +msgctxt "#30126" +msgid "Add Set" +msgstr "" + +msgctxt "#30127" +msgid "Delete Set" +msgstr "" + +msgctxt "#30128" +msgid "Are you sure you want to delete?" +msgstr "" + +msgctxt "#30129" +msgid "Exclude" +msgstr "" + +msgctxt "#30130" +msgid "The root folder cannot be changed" +msgstr "" + +msgctxt "#30131" +msgid "Choose Sets to Restore" +msgstr "" + +msgctxt "#30132" +msgid "Version 1.1.4 requires you to setup your file selections again - this is a breaking change" +msgstr "" + +msgctxt "#30133" +msgid "Game Saves" +msgstr "" + +msgctxt "#30134" +msgid "Include" +msgstr "" + +msgctxt "#30135" +msgid "Add Include Folder" +msgstr "" + +msgctxt "#30136" +msgid "Path must be within root folder" +msgstr "" + +msgctxt "#30137" +msgid "This path is part of a rule already" +msgstr "" + +msgctxt "#30138" +msgid "Set Name exists already" +msgstr "" + +msgctxt "#30139" +msgid "Copy Simple Config" +msgstr "" + +msgctxt "#30140" +msgid "This will copy the default Simple file selection to the Advanced Editor" +msgstr "" + +msgctxt "#30141" +msgid "This will erase any current Advanced Editor settings" +msgstr "" \ No newline at end of file diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 6a38749..eb300b9 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -129,14 +129,6 @@ msgctxt "#30035" msgid "Config Files" msgstr "Config Files" -msgctxt "#30036" -msgid "Custom Directory 1" -msgstr "Custom Directory 1" - -msgctxt "#30037" -msgid "Custom Directory 2" -msgstr "Custom Directory 2" - msgctxt "#30038" msgid "Advanced Settings Detected" msgstr "Advanced Settings Detected" diff --git a/resources/lib/advanced_editor.py b/resources/lib/advanced_editor.py new file mode 100644 index 0000000..ee697d2 --- /dev/null +++ b/resources/lib/advanced_editor.py @@ -0,0 +1,229 @@ +import json +import utils as utils +import xbmcvfs +import xbmc +import xbmcgui + +class BackupSetManager: + jsonFile = xbmc.translatePath(utils.data_dir() + "custom_paths.json") + paths = None + + def __init__(self): + self.paths = {} + + #try and read in the custom file + self._readFile() + + def addSet(self,aSet): + self.paths[aSet['name']] = {'root':aSet['root'],'dirs':[{"type":"include","path":aSet['root'],'recurse':True}]} + + #save the file + self._writeFile() + + def updateSet(self,name,aSet): + self.paths[name] = aSet + + #save the file + self._writeFile() + + def deleteSet(self,index): + #match the index to a key + keys = self.getSets() + + #delete this set + del self.paths[keys[index]] + + #save the file + self._writeFile() + + def getSets(self): + #list all current sets by name + keys = self.paths.keys() + keys.sort() + + return keys + + def getSet(self,index): + keys = self.getSets(); + + #return the set at this index + return {'name':keys[index],'set':self.paths[keys[index]]} + + def validateSetName(self,name): + return (name not in self.getSets()) + + def _writeFile(self): + #create the custom file + aFile = xbmcvfs.File(self.jsonFile,'w') + aFile.write(json.dumps(self.paths)) + aFile.close() + + def _readFile(self): + + if(xbmcvfs.exists(self.jsonFile)): + + #read in the custom file + aFile = xbmcvfs.File(self.jsonFile) + + #load custom dirs + self.paths = json.loads(aFile.read()) + aFile.close() + else: + #write a blank file + self._writeFile() + +class AdvancedBackupEditor: + dialog = None + + def __init__(self): + self.dialog = xbmcgui.Dialog() + + def _cleanPath(self,root,path): + return path[len(root)-1:] + + def _validatePath(self,root,path): + return path.startswith(root) + + def createSet(self): + backupSet = None + + name = self.dialog.input(utils.getString(30110),defaultt='Backup Set') + + if(name != None): + + #give a choice to start in home or enter a root path + enterHome = self.dialog.yesno(utils.getString(30111),line1=utils.getString(30112) + " - " + utils.getString(30114),line2=utils.getString(30113) + " - " + utils.getString(30115),nolabel=utils.getString(30112),yeslabel=utils.getString(30113)) + + rootFolder = 'special://home' + if(enterHome): + rootFolder = self.dialog.input(utils.getString(30116),defaultt=rootFolder) + + #direcotry has to end in slash + if(rootFolder[:-1] != '/'): + rootFolder = rootFolder + '/' + + #check that this path even exists + if(not xbmcvfs.exists(xbmc.translatePath(rootFolder))): + self.dialog.ok(utils.getString(30117),utils.getString(30118),rootFolder) + return None + else: + #select path to start set + rootFolder = self.dialog.browse(type=0,heading=utils.getString(30119),shares='files',defaultt=rootFolder) + + backupSet = {'name':name,'root':rootFolder} + + return backupSet + + def editSet(self,name,backupSet): + optionSelected = '' + rootPath = backupSet['root'] + utils.log(rootPath) + while(optionSelected != -1): + options = [xbmcgui.ListItem(utils.getString(30120),"Exclude a specific folder from this backup set"),xbmcgui.ListItem(utils.getString(30135),"Include a specific folder to this backup set"),xbmcgui.ListItem(rootPath,utils.getString(30121))] + + for aDir in backupSet['dirs']: + if(aDir['type'] == 'exclude'): + options.append(xbmcgui.ListItem(self._cleanPath(rootPath,aDir['path']),"%s: %s" % ("Type",utils.getString(30129)))) + elif(aDir['type'] == 'include'): + options.append(xbmcgui.ListItem(self._cleanPath(rootPath,aDir['path']),"%s: %s | %s: %s" % ("Type",utils.getString(30134),"Include Sub Folders",str(aDir['recurse'])))) + + optionSelected = self.dialog.select(utils.getString(30122) + ' ' + name,options,useDetails=True) + + if(optionSelected == 0 or optionSelected == 1): + #add a folder, will equal root if cancel is hit + addFolder = self.dialog.browse(type=0,heading=utils.getString(30120),shares='files',defaultt=backupSet['root']) + + if(addFolder.startswith(rootPath)): + + if(not any(addFolder == aDir['path'] for aDir in backupSet['dirs'])): + #cannot add root as an exclusion + if(optionSelected == 0 and addFolder != backupSet['root']): + backupSet['dirs'].append({"path":addFolder,"type":"exclude"}) + elif(optionSelected == 1): + #can add root as inclusion + backupSet['dirs'].append({"path":addFolder,"type":"include","recurse":True}) + else: + #this path is already part of another include/exclude rule + self.dialog.ok(utils.getString(30117),utils.getString(30137),addFolder) + else: + #folder must be under root folder + self.dialog.ok(utils.getString(30117), utils.getString(30136),rootPath) + elif(optionSelected == 2): + self.dialog.ok(utils.getString(30121),utils.getString(30130),backupSet['root']) + elif(optionSelected > 2): + + cOptions = ['Delete'] + if(backupSet['dirs'][optionSelected - 3]['type'] == 'include'): + cOptions.append('Toggle Sub Folders') + + contextOption = self.dialog.contextmenu(cOptions) + + if(contextOption == 0): + if(self.dialog.yesno(heading=utils.getString(30123),line1=utils.getString(30128))): + #remove folder + del backupSet['dirs'][optionSelected - 3] + elif(contextOption == 1 and backupSet['dirs'][optionSelected - 3]['type'] == 'include'): + #toggle if this folder should be recursive + backupSet['dirs'][optionSelected - 3]['recurse'] = not backupSet['dirs'][optionSelected - 3]['recurse'] + + return backupSet + + + def showMainScreen(self): + exitCondition = "" + customPaths = BackupSetManager() + + #show this every time + self.dialog.ok(utils.getString(30036),utils.getString(30037)) + + while(exitCondition != -1): + #load the custom paths + options = [xbmcgui.ListItem(utils.getString(30126),'',utils.addon_dir() + '/resources/images/plus-icon.png')] + + for index in range(0,len(customPaths.getSets())): + aSet = customPaths.getSet(index) + options.append(xbmcgui.ListItem(aSet['name'],utils.getString(30121) + ': ' + aSet['set']['root'],utils.addon_dir() + '/resources/images/folder-icon.png')) + + #show the gui + exitCondition = self.dialog.select(utils.getString(30125),options,useDetails=True) + + if(exitCondition >= 0): + if(exitCondition == 0): + newSet = self.createSet() + + #check that the name is unique + if(customPaths.validateSetName(newSet['name'])): + customPaths.addSet(newSet) + else: + self.dialog.ok(utils.getString(30117), utils.getString(30138),newSet['name']) + else: + #bring up a context menu + menuOption = self.dialog.contextmenu([utils.getString(30122),utils.getString(30123)]) + + if(menuOption == 0): + #get the set + aSet = customPaths.getSet(exitCondition -1) + + #edit the set + updatedSet = self.editSet(aSet['name'],aSet['set']) + + #save it + customPaths.updateSet(aSet['name'],updatedSet) + + elif(menuOption == 1): + if(self.dialog.yesno(heading=utils.getString(30127),line1=utils.getString(30128))): + #delete this path - subtract one because of "add" item + customPaths.deleteSet(exitCondition -1) + + def copySimpleConfig(self): + #disclaimer in case the user hit this on accident + shouldContinue = self.dialog.yesno(utils.getString(30139),utils.getString(30140),utils.getString(30141)) + + if(shouldContinue): + source = xbmc.translatePath(utils.addon_dir() + "/resources/data/default_files.json") + dest = xbmc.translatePath(utils.data_dir() + "/custom_paths.json") + + xbmcvfs.copy(source,dest) + + + diff --git a/resources/lib/authorizers.py b/resources/lib/authorizers.py index cda56eb..0364afe 100644 --- a/resources/lib/authorizers.py +++ b/resources/lib/authorizers.py @@ -3,9 +3,18 @@ import xbmcgui import xbmcvfs import resources.lib.tinyurl as tinyurl import resources.lib.utils as utils -import dropbox -from resources.lib.pydrive.auth import GoogleAuth -from resources.lib.pydrive.drive import GoogleDrive + +#don't die on import error yet, these might not even get used +try: + import dropbox +except ImportError: + pass + +try: + from resources.lib.pydrive.auth import GoogleAuth + from resources.lib.pydrive.drive import GoogleDrive +except ImportError: + pass class DropboxAuthorizer: APP_KEY = "" diff --git a/resources/lib/backup.py b/resources/lib/backup.py index 907a41b..2d7a4ff 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -1,656 +1,626 @@ -import xbmc -import xbmcgui -import xbmcvfs -import utils as utils -import time -import json -from vfs import XBMCFileSystem,DropboxFileSystem,ZipFileSystem,GoogleDriveFilesystem -from progressbar import BackupProgressBar -from resources.lib.guisettings import GuiSettingsManager -from resources.lib.extractor import ZipExtractor - -def folderSort(aKey): - result = aKey[0] - - if(len(result) < 8): - result = result + "0000" - - return result - - -class XbmcBackup: - #constants for initiating a back or restore - Backup = 0 - Restore = 1 - - #file systems - xbmc_vfs = None - remote_vfs = None - saved_remote_vfs = None - - restoreFile = None - remote_base_path = None - - #for the progress bar - progressBar = None - filesLeft = 0 - filesTotal = 1 - - fileManager = None - restore_point = None - skip_advanced = False #if we should check for the existance of advancedsettings in the restore - - def __init__(self): - self.xbmc_vfs = XBMCFileSystem(xbmc.translatePath('special://home')) - - self.configureRemote() - utils.log(utils.getString(30046)) - - def configureRemote(self): - if(utils.getSetting('remote_selection') == '1'): - self.remote_base_path = utils.getSetting('remote_path_2'); - self.remote_vfs = XBMCFileSystem(utils.getSetting('remote_path_2')) - utils.setSetting("remote_path","") - elif(utils.getSetting('remote_selection') == '0'): - self.remote_base_path = utils.getSetting('remote_path'); - self.remote_vfs = XBMCFileSystem(utils.getSetting("remote_path")) - elif(utils.getSetting('remote_selection') == '2'): - self.remote_base_path = "/" - self.remote_vfs = DropboxFileSystem("/") - elif(utils.getSetting('remote_selection') == '3'): - self.remote_base_path = '/Kodi Backup/' - self.remote_vfs = GoogleDriveFilesystem('/Kodi Backup/') - - def remoteConfigured(self): - result = True - - if(self.remote_base_path == ""): - result = False - - return result - - def listBackups(self): - result = [] - - #get all the folders in the current root path - dirs,files = self.remote_vfs.listdir(self.remote_base_path) - - for aDir in dirs: - if(self.remote_vfs.exists(self.remote_base_path + aDir + "/xbmcbackup.val")): - - #folder may or may not contain time, older versions didn't include this - folderName = '' - if(len(aDir) > 8): - folderName = aDir[6:8] + '-' + aDir[4:6] + '-' + aDir[0:4] + " " + aDir[8:10] + ":" + aDir[10:12] - else: - folderName = aDir[6:8] + '-' + aDir[4:6] + '-' + aDir[0:4] - - result.append((aDir,folderName)) - - for aFile in files: - file_ext = aFile.split('.')[-1] - folderName = utils.encode(aFile.split('.')[0]) - - if(file_ext == 'zip' and (len(folderName) == 12 or len(folderName) == 8) and str.isdigit(folderName)): - - #folder may or may not contain time, older versions didn't include this - if(len(aFile ) > 8): - folderName = aFile [6:8] + '-' + aFile [4:6] + '-' + aFile [0:4] + " " + aFile [8:10] + ":" + aFile [10:12] - else: - folderName = aFile [6:8] + '-' + aFile [4:6] + '-' + aFile [0:4] - - result.append((aFile ,folderName)) - - - result.sort(key=folderSort) - - return result - - def selectRestore(self,restore_point): - self.restore_point = restore_point - - def skipAdvanced(self): - self.skip_advanced = True - - def run(self,mode=-1,progressOverride=False): - #set windows setting to true - window = xbmcgui.Window(10000) - window.setProperty(utils.__addon_id__ + ".running","true") - - #append backup folder name - progressBarTitle = utils.getString(30010) + " - " - if(mode == self.Backup and self.remote_vfs.root_path != ''): - if(utils.getSetting("compress_backups") == 'true'): - #delete old temp file - if(self.xbmc_vfs.exists(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))): - if(not self.xbmc_vfs.rmfile(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))): - #we had some kind of error deleting the old file - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30096),utils.getString(30097)) - return - - #save the remote file system and use the zip vfs - self.saved_remote_vfs = self.remote_vfs - self.remote_vfs = ZipFileSystem(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"),"w") - - self.remote_vfs.set_root(self.remote_vfs.root_path + time.strftime("%Y%m%d%H%M") + "/") - progressBarTitle = progressBarTitle + utils.getString(30023) + ": " + utils.getString(30016) - elif(mode == self.Restore and self.restore_point != None and self.remote_vfs.root_path != ''): - if(self.restore_point.split('.')[-1] != 'zip'): - self.remote_vfs.set_root(self.remote_vfs.root_path + self.restore_point + "/") - progressBarTitle = progressBarTitle + utils.getString(30023) + ": " + utils.getString(30017) - else: - #kill the program here - self.remote_vfs = None - return - - utils.log(utils.getString(30047) + ": " + self.xbmc_vfs.root_path) - utils.log(utils.getString(30048) + ": " + self.remote_vfs.root_path) - - - #setup the progress bar - self.progressBar = BackupProgressBar(progressOverride) - self.progressBar.create(progressBarTitle,utils.getString(30049) + "......") - - if(mode == self.Backup): - utils.log(utils.getString(30023) + " - " + utils.getString(30016)) - #check if remote path exists - if(self.remote_vfs.exists(self.remote_vfs.root_path)): - #may be data in here already - utils.log(utils.getString(30050)) - else: - #make the remote directory - self.remote_vfs.mkdir(self.remote_vfs.root_path) - - #create a validation file for backup rotation - writeCheck = self._createValidationFile() - - if(not writeCheck): - #we may not be able to write to this destination for some reason - shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30089),utils.getString(30090), utils.getString(30044),autoclose=25000) - - if(not shouldContinue): - return - - utils.log(utils.getString(30051)) - allFiles = [] - fileManager = FileManager(self.xbmc_vfs) - - #go through each of the user selected items and write them to the backup store - if(utils.getSetting('backup_addons') == 'true'): - fileManager.addFile("-" + xbmc.translatePath('special://home/addons')) - fileManager.excludeFile(xbmc.translatePath('special://home/addons/packages')) - fileManager.walkTree(xbmc.translatePath('special://home/addons')) - - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata')) - - if(utils.getSetting('backup_addon_data') == 'true'): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/addon_data')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/addon_data')) - - if(utils.getSetting('backup_database') == 'true'): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/Database')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/Database')) - - if(utils.getSetting("backup_playlists") == 'true'): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/playlists')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/playlists')) - - if(utils.getSetting('backup_profiles') == 'true'): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/profiles')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/profiles')) - - if(utils.getSetting("backup_thumbnails") == "true"): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/Thumbnails')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/Thumbnails')) - - if(utils.getSetting("backup_config") == "true"): - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/keymaps')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/keymaps')) - - fileManager.addFile("-" + xbmc.translatePath('special://home/userdata/peripheral_data')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/peripheral_data')) - - fileManager.addFile('-' + xbmc.translatePath('special://home/userdata/library')) - fileManager.walkTree(xbmc.translatePath('special://home/userdata/library')) - - #this part is an oddity - dirs,configFiles = self.xbmc_vfs.listdir(xbmc.translatePath('special://home/userdata/')) - for aFile in configFiles: - if(aFile.endswith(".xml")): - fileManager.addFile(xbmc.translatePath('special://home/userdata/') + aFile) - - #add to array - self.filesTotal = fileManager.size() - allFiles.append({"source":self.xbmc_vfs.root_path,"dest":self.remote_vfs.root_path,"files":fileManager.getFiles()}) - - orig_base_path = self.remote_vfs.root_path - - #check if there are custom directories - if(utils.getSetting('custom_dir_1_enable') == 'true' and utils.getSetting('backup_custom_dir_1') != ''): - - #create a special remote path with hash - self.xbmc_vfs.set_root(utils.getSetting('backup_custom_dir_1')) - fileManager.addFile("-custom_" + self._createCRC(self.xbmc_vfs.root_path)) - - #walk the directory - fileManager.walkTree(self.xbmc_vfs.root_path) - self.filesTotal = self.filesTotal + fileManager.size() - allFiles.append({"source":self.xbmc_vfs.root_path,"dest":self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path),"files":fileManager.getFiles()}) - - if(utils.getSetting('custom_dir_2_enable') == 'true' and utils.getSetting('backup_custom_dir_2') != ''): - - #create a special remote path with hash - self.xbmc_vfs.set_root(utils.getSetting('backup_custom_dir_2')) - fileManager.addFile("-custom_" + self._createCRC(self.xbmc_vfs.root_path)) - - #walk the directory - fileManager.walkTree(self.xbmc_vfs.root_path) - self.filesTotal = self.filesTotal + fileManager.size() - allFiles.append({"source":self.xbmc_vfs.root_path,"dest":self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path),"files":fileManager.getFiles()}) - - - #backup all the files - self.filesLeft = self.filesTotal - for fileGroup in allFiles: - self.xbmc_vfs.set_root(fileGroup['source']) - self.remote_vfs.set_root(fileGroup['dest']) - filesCopied = self.backupFiles(fileGroup['files'],self.xbmc_vfs,self.remote_vfs) - - if(not filesCopied): - utils.showNotification(utils.getString(30092)) - utils.log(utils.getString(30092)) - - #reset remote and xbmc vfs - self.xbmc_vfs.set_root("special://home/") - self.remote_vfs.set_root(orig_base_path) - - if(utils.getSetting("compress_backups") == 'true'): - #send the zip file to the real remote vfs - zip_name = self.remote_vfs.root_path[:-1] + ".zip" - self.remote_vfs.cleanup() - self.xbmc_vfs.rename(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"), xbmc.translatePath("special://temp/" + zip_name)) - fileManager.addFile(xbmc.translatePath("special://temp/" + zip_name)) - - #set root to data dir home - self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/")) - - self.remote_vfs = self.saved_remote_vfs - self.progressBar.updateProgress(98, utils.getString(30088)) - fileCopied = self.backupFiles(fileManager.getFiles(),self.xbmc_vfs, self.remote_vfs) - - if(not fileCopied): - #zip archive copy filed, inform the user - shouldContinue = xbmcgui.Dialog().ok(utils.getString(30089),utils.getString(30090), utils.getString(30091)) - - #delete the temp zip file - self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + zip_name)) - - #remove old backups - self._rotateBackups() - - elif (mode == self.Restore): - utils.log(utils.getString(30023) + " - " + utils.getString(30017)) - - #catch for if the restore point is actually a zip file - if(self.restore_point.split('.')[-1] == 'zip'): - self.progressBar.updateProgress(2, utils.getString(30088)) - utils.log("copying zip file: " + self.restore_point) - - #set root to data dir home - self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/")) - - if(not self.xbmc_vfs.exists(xbmc.translatePath("special://temp/" + self.restore_point))): - #copy just this file from the remote vfs - zipFile = [] - zipFile.append(self.remote_base_path + self.restore_point) - - self.backupFiles(zipFile,self.remote_vfs, self.xbmc_vfs) - else: - utils.log("zip file exists already") - - #extract the zip file - zip_vfs = ZipFileSystem(xbmc.translatePath("special://temp/"+ self.restore_point),'r') - extractor = ZipExtractor() - - if(not extractor.extract(zip_vfs, xbmc.translatePath("special://temp/"), self.progressBar)): - #we had a problem extracting the archive, delete everything - zip_vfs.cleanup() - self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point)) - - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30101)) - return - - zip_vfs.cleanup() - - self.progressBar.updateProgress(0,utils.getString(30049) + "......") - #set the new remote vfs and fix xbmc path - self.remote_vfs = XBMCFileSystem(xbmc.translatePath("special://temp/" + self.restore_point.split(".")[0] + "/")) - self.xbmc_vfs.set_root(xbmc.translatePath("special://home/")) - - #for restores remote path must exist - if(not self.remote_vfs.exists(self.remote_vfs.root_path)): - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045),self.remote_vfs.root_path) - return - - if(not self._checkValidationFile(self.remote_vfs.root_path)): - #don't continue - return - - utils.log(utils.getString(30051)) - allFiles = [] - fileManager = FileManager(self.remote_vfs) - - #go through each of the user selected items and write them to the backup store - - if(utils.getSetting("backup_config") == "true"): - #check for the existance of an advancedsettings file - if(self.remote_vfs.exists(self.remote_vfs.root_path + "userdata/advancedsettings.xml") and not self.skip_advanced): - #let the user know there is an advanced settings file present - restartXbmc = xbmcgui.Dialog().yesno(utils.getString(30038),utils.getString(30039),utils.getString(30040), utils.getString(30041)) - - if(restartXbmc): - #add only this file to the file list - fileManager.addFile(self.remote_vfs.root_path + "userdata/advancedsettings.xml") - self.backupFiles(fileManager.getFiles(),self.remote_vfs,self.xbmc_vfs) - - #let the service know to resume this backup on startup - self._createResumeBackupFile() - - #do not continue running - xbmcgui.Dialog().ok(utils.getString(30077),utils.getString(30078)) - - return - - fileManager.addFile('-' + self.remote_vfs.root_path + 'userdata/keymaps') - fileManager.walkTree(self.remote_vfs.root_path + "userdata/keymaps") - - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/peripheral_data") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/peripheral_data") - - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/library") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/library") - - #this part is an oddity - dirs,configFiles = self.remote_vfs.listdir(self.remote_vfs.root_path + "userdata/") - for aFile in configFiles: - if(aFile.endswith(".xml")): - fileManager.addFile(self.remote_vfs.root_path + "userdata/" + aFile) - - if(utils.getSetting('backup_addons') == 'true'): - fileManager.addFile('-' + self.remote_vfs.root_path + "addons") - fileManager.walkTree(self.remote_vfs.root_path + "addons") - - self.xbmc_vfs.mkdir(xbmc.translatePath('special://home/userdata')) - - if(utils.getSetting('backup_addon_data') == 'true'): - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/addon_data") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/addon_data") - - if(utils.getSetting('backup_database') == 'true'): - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/Database") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/Database") - - if(utils.getSetting("backup_playlists") == 'true'): - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/playlists") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/playlists") - - if(utils.getSetting('backup_profiles') == 'true'): - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/profiles") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/profiles") - - if(utils.getSetting("backup_thumbnails") == "true"): - fileManager.addFile('-' + self.remote_vfs.root_path + "userdata/Thumbnails") - fileManager.walkTree(self.remote_vfs.root_path + "userdata/Thumbnails") - - #add to array - self.filesTotal = fileManager.size() - allFiles.append({"source":self.remote_vfs.root_path,"dest":self.xbmc_vfs.root_path,"files":fileManager.getFiles()}) - - #check if there are custom directories - if(utils.getSetting('custom_dir_1_enable') == 'true' and utils.getSetting('backup_custom_dir_1') != ''): - - self.xbmc_vfs.set_root(utils.getSetting('backup_custom_dir_1')) - if(self.remote_vfs.exists(self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path) + "/")): - #index files to restore - fileManager.walkTree(self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path)) - self.filesTotal = self.filesTotal + fileManager.size() - allFiles.append({"source":self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path),"dest":self.xbmc_vfs.root_path,"files":fileManager.getFiles()}) - else: - utils.log("error path not found: " + self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path)) - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045),self.remote_vfs.root_path + "custom_" + self._createCRC(utils.getSetting('backup_custom_dir_1'))) - - if(utils.getSetting('custom_dir_2_enable') == 'true' and utils.getSetting('backup_custom_dir_2') != ''): - - self.xbmc_vfs.set_root(utils.getSetting('backup_custom_dir_2')) - if(self.remote_vfs.exists(self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path) + "/")): - #index files to restore - fileManager.walkTree(self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path)) - self.filesTotal = self.filesTotal + fileManager.size() - allFiles.append({"source":self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path),"dest":self.xbmc_vfs.root_path,"files":fileManager.getFiles()}) - else: - utils.log("error path not found: " + self.remote_vfs.root_path + "custom_" + self._createCRC(self.xbmc_vfs.root_path)) - xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045),self.remote_vfs.root_path + "custom_" + self._createCRC(utils.getSetting('backup_custom_dir_2'))) - - - #restore all the files - self.filesLeft = self.filesTotal - for fileGroup in allFiles: - self.remote_vfs.set_root(fileGroup['source']) - self.xbmc_vfs.set_root(fileGroup['dest']) - self.backupFiles(fileGroup['files'],self.remote_vfs,self.xbmc_vfs) - - self.progressBar.updateProgress(99,"Clean up operations .....") - - if(self.restore_point.split('.')[-1] == 'zip'): - #delete the zip file and the extracted directory - self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point)) - self.xbmc_vfs.rmdir(self.remote_vfs.root_path) - - if(utils.getSetting("backup_config") == "true"): - #update the guisettings information (or what we can from it) - gui_settings = GuiSettingsManager('special://home/userdata/guisettings.xml') - gui_settings.run() - - #call update addons to refresh everything - xbmc.executebuiltin('UpdateLocalAddons') - - self.xbmc_vfs.cleanup() - self.remote_vfs.cleanup() - self.progressBar.close() - - #reset the window setting - window.setProperty(utils.__addon_id__ + ".running","") - - def backupFiles(self,fileList,source,dest): - result = True - - utils.log("Writing files to: " + dest.root_path) - utils.log("Source: " + source.root_path) - for aFile in fileList: - if(not self.progressBar.checkCancel()): - utils.log('Writing file: ' + aFile,xbmc.LOGDEBUG) - if(aFile.startswith("-")): - self._updateProgress(aFile[len(source.root_path) + 1:]) - dest.mkdir(dest.root_path + aFile[len(source.root_path) + 1:]) - else: - self._updateProgress() - wroteFile = True - if(isinstance(source,DropboxFileSystem) or isinstance(source,GoogleDriveFilesystem)): - #if copying from cloud storage we need the file handle, use get_file - wroteFile = source.get_file(aFile,dest.root_path + aFile[len(source.root_path):]) - else: - #copy using normal method - wroteFile = dest.put(aFile,dest.root_path + aFile[len(source.root_path):]) - - #if result is still true but this file failed - if(not wroteFile and result): - result = False - - - return result - - def _createCRC(self,string): - #create hash from string - string = string.lower() - bytes = bytearray(string.encode()) - crc = 0xffffffff; - for b in bytes: - crc = crc ^ (b << 24) - for i in range(8): - if (crc & 0x80000000 ): - crc = (crc << 1) ^ 0x04C11DB7 - else: - crc = crc << 1; - crc = crc & 0xFFFFFFFF - - return '%08x' % crc - - def _updateProgress(self,message=None): - self.filesLeft = self.filesLeft - 1 - self.progressBar.updateProgress(int((float(self.filesTotal - self.filesLeft)/float(self.filesTotal)) * 100),message) - - def _rotateBackups(self): - total_backups = int(utils.getSetting('backup_rotation')) - - if(total_backups > 0): - #get a list of valid backup folders - dirs = self.listBackups() - - if(len(dirs) > total_backups): - #remove backups to equal total wanted - remove_num = 0 - self.filesTotal = self.filesTotal + remove_num + 1 - - #update the progress bar if it is available - while(remove_num < (len(dirs) - total_backups) and not self.progressBar.checkCancel()): - self._updateProgress(utils.getString(30054) + " " + dirs[remove_num][1]) - utils.log("Removing backup " + dirs[remove_num][0]) - - if(dirs[remove_num][0].split('.')[-1] == 'zip'): - #this is a file, remove it that way - self.remote_vfs.rmfile(self.remote_base_path + dirs[remove_num][0]) - else: - self.remote_vfs.rmdir(self.remote_base_path + dirs[remove_num][0] + "/") - - remove_num = remove_num + 1 - - def _createValidationFile(self): - vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"),'w') - vFile.write(json.dumps({"name":"XBMC Backup Validation File","xbmc_version":xbmc.getInfoLabel('System.BuildVersion')})) - vFile.write("") - vFile.close() - - success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"),self.remote_vfs.root_path + "xbmcbackup.val") - - #remove the validation file - xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val")) - - if(success): - #android requires a .nomedia file to not index the directory as media - if(not xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + ".nomedia"))): - nmFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + ".nomedia"),'w') - nmFile.close() - - success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + ".nomedia"),self.remote_vfs.root_path + ".nomedia") - - return success - - def _checkValidationFile(self,path): - result = False - - #copy the file and open it - self.xbmc_vfs.put(path + "xbmcbackup.val",xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val")) - - vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"),'r') - jsonString = vFile.read() - vFile.close() - - #delete after checking - xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val")) - - try: - json_dict = json.loads(jsonString) - - if(xbmc.getInfoLabel('System.BuildVersion') == json_dict['xbmc_version']): - result = True - else: - result = xbmcgui.Dialog().yesno(utils.getString(30085),utils.getString(30086),utils.getString(30044)) - - except ValueError: - #may fail on older archives - result = True - - return result - - def _createResumeBackupFile(self): - rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'w') - rFile.write(self.restore_point) - rFile.close() - -class FileManager: - not_dir = ['.zip','.xsp','.rar'] - exclude_dir = [] - - def __init__(self,vfs): - self.vfs = vfs - self.fileArray = [] - - def walkTree(self,directory): - - if(directory[-1:] == '/'): - directory = directory[:-1] - - if(self.vfs.exists(directory + "/")): - dirs,files = self.vfs.listdir(directory) - - #create all the subdirs first - for aDir in dirs: - dirPath = xbmc.validatePath(xbmc.translatePath(directory + "/" + aDir)) - file_ext = aDir.split('.')[-1] - - if(not dirPath in self.exclude_dir): - - self.addFile("-" + dirPath) - - #catch for "non directory" type files - shouldWalk = True - - for s in file_ext: - if(s in self.not_dir): - shouldWalk = False - - if(shouldWalk): - self.walkTree(dirPath) - - #copy all the files - for aFile in files: - filePath = xbmc.translatePath(directory + "/" + aFile) - self.addFile(filePath) - - def addFile(self,filename): - try: - filename = filename.decode('UTF-8') - except UnicodeDecodeError: - filename = filename.decode('ISO-8859-2') - - #write the full remote path name of this file - utils.log("Add File: " + filename,xbmc.LOGDEBUG) - self.fileArray.append(filename) - - def excludeFile(self,filename): - try: - filename = filename.decode('UTF-8') - except UnicodeDecodeError: - filename = filename.decode('ISO-8859-2') - - #write the full remote path name of this file - utils.log("Exclude File: " + filename,xbmc.LOGDEBUG) - self.exclude_dir.append(filename) - - def getFiles(self): - result = self.fileArray - self.fileArray = [] - return result - - def size(self): - return len(self.fileArray) +import xbmc +import xbmcgui +import xbmcvfs +import utils as utils +import time +import json +from datetime import datetime +from vfs import XBMCFileSystem,DropboxFileSystem,ZipFileSystem,GoogleDriveFilesystem +from progressbar import BackupProgressBar +from resources.lib.guisettings import GuiSettingsManager +from resources.lib.extractor import ZipExtractor +from __builtin__ import file + +def folderSort(aKey): + result = aKey[0] + + if(len(result) < 8): + result = result + "0000" + + return result + + +class XbmcBackup: + #constants for initiating a back or restore + Backup = 0 + Restore = 1 + + #list of dirs for the "simple" file selection + simple_directory_list = ['addons','addon_data','database','game_saves','playlists','profiles','thumbnails','config'] + + #file systems + xbmc_vfs = None + remote_vfs = None + saved_remote_vfs = None + + restoreFile = None + remote_base_path = None + + #for the progress bar + progressBar = None + filesLeft = 0 + filesTotal = 1 + + restore_point = None + skip_advanced = False #if we should check for the existance of advancedsettings in the restore + + def __init__(self): + self.xbmc_vfs = XBMCFileSystem(xbmc.translatePath('special://home')) + + self.configureRemote() + utils.log(utils.getString(30046)) + + def configureRemote(self): + if(utils.getSetting('remote_selection') == '1'): + self.remote_base_path = utils.getSetting('remote_path_2'); + self.remote_vfs = XBMCFileSystem(utils.getSetting('remote_path_2')) + utils.setSetting("remote_path","") + elif(utils.getSetting('remote_selection') == '0'): + self.remote_base_path = utils.getSetting('remote_path'); + self.remote_vfs = XBMCFileSystem(utils.getSetting("remote_path")) + elif(utils.getSetting('remote_selection') == '2'): + self.remote_base_path = "/" + self.remote_vfs = DropboxFileSystem("/") + elif(utils.getSetting('remote_selection') == '3'): + self.remote_base_path = '/Kodi Backup/' + self.remote_vfs = GoogleDriveFilesystem('/Kodi Backup/') + + def remoteConfigured(self): + result = True + + if(self.remote_base_path == ""): + result = False + + return result + + #reverse - should reverse the resulting, default is true - newest to oldest + def listBackups(self,reverse=True): + result = [] + + #get all the folders in the current root path + dirs,files = self.remote_vfs.listdir(self.remote_base_path) + + for aDir in dirs: + if(self.remote_vfs.exists(self.remote_base_path + aDir + "/xbmcbackup.val")): + + #format the name according to regional settings + folderName = self._dateFormat(aDir) + + result.append((aDir,folderName)) + + for aFile in files: + file_ext = aFile.split('.')[-1] + folderName = utils.encode(aFile.split('.')[0]) + + if(file_ext == 'zip' and len(folderName) == 12 and str.isdigit(folderName)): + + #format the name according to regional settings + folderName = self._dateFormat(folderName) + + result.append((aFile ,folderName)) + + + result.sort(key=folderSort,reverse=reverse) + + return result + + def selectRestore(self,restore_point): + self.restore_point = restore_point + + def skipAdvanced(self): + self.skip_advanced = True + + def backup(self,progressOverride=False): + shouldContinue = self._setupVFS(self.Backup,progressOverride) + + if(shouldContinue): + utils.log(utils.getString(30023) + " - " + utils.getString(30016)) + #check if remote path exists + if(self.remote_vfs.exists(self.remote_vfs.root_path)): + #may be data in here already + utils.log(utils.getString(30050)) + else: + #make the remote directory + self.remote_vfs.mkdir(self.remote_vfs.root_path) + + utils.log(utils.getString(30051)) + utils.log('File Selection Type: ' + str(utils.getSetting('backup_selection_type'))) + allFiles = [] + + if(int(utils.getSetting('backup_selection_type')) == 0): + #read in a list of the directories to backup + selectedDirs = self._readBackupConfig(utils.addon_dir() + "/resources/data/default_files.json") + + #simple mode - get file listings for all enabled directories + for aDir in self.simple_directory_list: + #if this dir enabled + if(utils.getSetting('backup_' + aDir) == 'true'): + #get a file listing and append it to the allfiles array + allFiles.append(self._addBackupDir(aDir,selectedDirs[aDir]['root'],selectedDirs[aDir]['dirs'])) + else: + #advanced mode - load custom paths + selectedDirs = self._readBackupConfig(utils.data_dir() + "/custom_paths.json") + + #get the set names + keys = selectedDirs.keys() + + #go through the custom sets + for aKey in keys: + #get the set + aSet = selectedDirs[aKey] + + #get file listing and append + allFiles.append(self._addBackupDir(aKey,aSet['root'],aSet['dirs'])) + + #create a validation file for backup rotation + writeCheck = self._createValidationFile(allFiles) + + if(not writeCheck): + #we may not be able to write to this destination for some reason + shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30089),utils.getString(30090), utils.getString(30044),autoclose=25000) + + if(not shouldContinue): + return + + orig_base_path = self.remote_vfs.root_path + + #backup all the files + self.filesLeft = self.filesTotal + for fileGroup in allFiles: + self.xbmc_vfs.set_root(xbmc.translatePath(fileGroup['source'])) + self.remote_vfs.set_root(fileGroup['dest'] + fileGroup['name']) + filesCopied = self._copyFiles(fileGroup['files'],self.xbmc_vfs,self.remote_vfs) + + if(not filesCopied): + utils.showNotification(utils.getString(30092)) + utils.log(utils.getString(30092)) + + #reset remote and xbmc vfs + self.xbmc_vfs.set_root("special://home/") + self.remote_vfs.set_root(orig_base_path) + + if(utils.getSetting("compress_backups") == 'true'): + fileManager = FileManager(self.xbmc_vfs) + + #send the zip file to the real remote vfs + zip_name = self.remote_vfs.root_path[:-1] + ".zip" + self.remote_vfs.cleanup() + self.xbmc_vfs.rename(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"), xbmc.translatePath("special://temp/" + zip_name)) + fileManager.addFile(xbmc.translatePath("special://temp/" + zip_name)) + + #set root to data dir home + self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/")) + + self.remote_vfs = self.saved_remote_vfs + self.progressBar.updateProgress(98, utils.getString(30088)) + fileCopied = self._copyFiles(fileManager.getFiles(),self.xbmc_vfs, self.remote_vfs) + + if(not fileCopied): + #zip archive copy filed, inform the user + shouldContinue = xbmcgui.Dialog().ok(utils.getString(30089),utils.getString(30090), utils.getString(30091)) + + #delete the temp zip file + self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + zip_name)) + + #remove old backups + self._rotateBackups() + + #close any files + self._closeVFS() + + def restore(self,progressOverride=False,selectedSets=None): + shouldContinue = self._setupVFS(self.Restore, progressOverride) + + if(shouldContinue): + utils.log(utils.getString(30023) + " - " + utils.getString(30017)) + + #catch for if the restore point is actually a zip file + if(self.restore_point.split('.')[-1] == 'zip'): + self.progressBar.updateProgress(2, utils.getString(30088)) + utils.log("copying zip file: " + self.restore_point) + + #set root to data dir home + self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/")) + + if(not self.xbmc_vfs.exists(xbmc.translatePath("special://temp/" + self.restore_point))): + #copy just this file from the remote vfs + zipFile = [] + zipFile.append(self.remote_base_path + self.restore_point) + + self._copyFiles(zipFile,self.remote_vfs, self.xbmc_vfs) + else: + utils.log("zip file exists already") + + #extract the zip file + zip_vfs = ZipFileSystem(xbmc.translatePath("special://temp/"+ self.restore_point),'r') + extractor = ZipExtractor() + + if(not extractor.extract(zip_vfs, xbmc.translatePath("special://temp/"), self.progressBar)): + #we had a problem extracting the archive, delete everything + zip_vfs.cleanup() + self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point)) + + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30101)) + return + + zip_vfs.cleanup() + + self.progressBar.updateProgress(0,utils.getString(30049) + "......") + #set the new remote vfs and fix xbmc path + self.remote_vfs = XBMCFileSystem(xbmc.translatePath("special://temp/" + self.restore_point.split(".")[0] + "/")) + self.xbmc_vfs.set_root(xbmc.translatePath("special://home/")) + + #for restores remote path must exist + if(not self.remote_vfs.exists(self.remote_vfs.root_path)): + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045),self.remote_vfs.root_path) + return + + valFile = self._checkValidationFile(self.remote_vfs.root_path) + if(valFile == None): + #don't continue + return + + utils.log(utils.getString(30051)) + allFiles = [] + fileManager = FileManager(self.remote_vfs) + + #check for the existance of an advancedsettings file + if(self.remote_vfs.exists(self.remote_vfs.root_path + "config/advancedsettings.xml") and not self.skip_advanced): + #let the user know there is an advanced settings file present + restartXbmc = xbmcgui.Dialog().yesno(utils.getString(30038),utils.getString(30039),utils.getString(30040), utils.getString(30041)) + + if(restartXbmc): + #add only this file to the file list + fileManager.addFile(self.remote_vfs.root_path + "config/advancedsettings.xml") + self._copyFiles(fileManager.getFiles(),self.remote_vfs,self.xbmc_vfs) + + #let the service know to resume this backup on startup + self._createResumeBackupFile() + + #do not continue running + xbmcgui.Dialog().ok(utils.getString(30077),utils.getString(30078)) + return + + #use a multiselect dialog to select sets to restore + restoreSets = [n['name'] for n in valFile['directories']] + + #if passed in list, skip selection + if(selectedSets == None): + selectedSets = xbmcgui.Dialog().multiselect(utils.getString(30131),restoreSets) + else: + selectedSets = [restoreSets.index(n) for n in selectedSets if n in restoreSets] #if set name not found just skip it + + if(selectedSets != None): + #go through each of the directories in the backup and write them to the correct location + for index in selectedSets: + + #add this directory + aDir = valFile['directories'][index] + + self.xbmc_vfs.set_root(xbmc.translatePath(aDir['path'])) + if(self.remote_vfs.exists(self.remote_vfs.root_path + aDir['name'] + '/')): + #walk the directory + fileManager.walkTree(self.remote_vfs.root_path + aDir['name'] + '/') + self.filesTotal = self.filesTotal + fileManager.size() + allFiles.append({"source":self.remote_vfs.root_path + aDir['name'],"dest":self.xbmc_vfs.root_path,"files":fileManager.getFiles()}) + else: + utils.log("error path not found: " + self.remote_vfs.root_path + aDir['name']) + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045),self.remote_vfs.root_path + aDir['name']) + + #restore all the files + self.filesLeft = self.filesTotal + for fileGroup in allFiles: + self.remote_vfs.set_root(fileGroup['source']) + self.xbmc_vfs.set_root(fileGroup['dest']) + self._copyFiles(fileGroup['files'],self.remote_vfs,self.xbmc_vfs) + + self.progressBar.updateProgress(99,"Clean up operations .....") + + if(self.restore_point.split('.')[-1] == 'zip'): + #delete the zip file and the extracted directory + self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + self.restore_point)) + self.xbmc_vfs.rmdir(self.remote_vfs.root_path) + + if(utils.getSetting("backup_config") == "true"): + #update the guisettings information (or what we can from it) + gui_settings = GuiSettingsManager('special://home/userdata/guisettings.xml') + gui_settings.run() + + #call update addons to refresh everything + xbmc.executebuiltin('UpdateLocalAddons') + + def _setupVFS(self,mode=-1,progressOverride=False): + #set windows setting to true + window = xbmcgui.Window(10000) + window.setProperty(utils.__addon_id__ + ".running","true") + + #append backup folder name + progressBarTitle = utils.getString(30010) + " - " + if(mode == self.Backup and self.remote_vfs.root_path != ''): + if(utils.getSetting("compress_backups") == 'true'): + #delete old temp file + if(self.xbmc_vfs.exists(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))): + if(not self.xbmc_vfs.rmfile(xbmc.translatePath('special://temp/xbmc_backup_temp.zip'))): + #we had some kind of error deleting the old file + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30096),utils.getString(30097)) + return False + + #save the remote file system and use the zip vfs + self.saved_remote_vfs = self.remote_vfs + self.remote_vfs = ZipFileSystem(xbmc.translatePath("special://temp/xbmc_backup_temp.zip"),"w") + + self.remote_vfs.set_root(self.remote_vfs.root_path + time.strftime("%Y%m%d%H%M") + "/") + progressBarTitle = progressBarTitle + utils.getString(30023) + ": " + utils.getString(30016) + elif(mode == self.Restore and self.restore_point != None and self.remote_vfs.root_path != ''): + if(self.restore_point.split('.')[-1] != 'zip'): + self.remote_vfs.set_root(self.remote_vfs.root_path + self.restore_point + "/") + progressBarTitle = progressBarTitle + utils.getString(30023) + ": " + utils.getString(30017) + else: + #kill the program here + self.remote_vfs = None + return False + + utils.log(utils.getString(30047) + ": " + self.xbmc_vfs.root_path) + utils.log(utils.getString(30048) + ": " + self.remote_vfs.root_path) + + + #setup the progress bar + self.progressBar = BackupProgressBar(progressOverride) + self.progressBar.create(progressBarTitle,utils.getString(30049) + "......") + + #if we made it this far we're good + return True + + def _closeVFS(self): + self.xbmc_vfs.cleanup() + self.remote_vfs.cleanup() + self.progressBar.close() + + #reset the window setting + window = xbmcgui.Window(10000) + window.setProperty(utils.__addon_id__ + ".running","") + + def _copyFiles(self,fileList,source,dest): + result = True + + utils.log("Source: " + source.root_path) + utils.log("Desintation: " + dest.root_path) + + #make sure the dest folder exists - can cause write errors if the full path doesn't exist + if(not dest.exists(dest.root_path)): + dest.mkdir(dest.root_path) + + for aFile in fileList: + if(not self.progressBar.checkCancel()): + utils.log('Writing file: ' + aFile,xbmc.LOGDEBUG) + if(aFile.startswith("-")): + self._updateProgress(aFile[len(source.root_path) + 1:]) + dest.mkdir(dest.root_path + aFile[len(source.root_path) + 1:]) + else: + self._updateProgress() + wroteFile = True + if(isinstance(source,DropboxFileSystem) or isinstance(source,GoogleDriveFilesystem)): + #if copying from cloud storage we need the file handle, use get_file + wroteFile = source.get_file(aFile,dest.root_path + aFile[len(source.root_path):]) + else: + #copy using normal method + wroteFile = dest.put(aFile,dest.root_path + aFile[len(source.root_path):]) + + #if result is still true but this file failed + if(not wroteFile and result): + result = False + + + return result + + def _addBackupDir(self,folder_name,root_path,dirList): + utils.log('Backup set: ' + folder_name) + fileManager = FileManager(self.xbmc_vfs) + + self.xbmc_vfs.set_root(xbmc.translatePath(root_path)) + for aDir in dirList: + fileManager.addDir(aDir) + + #walk all the root trees + fileManager.walk() + #update total files + self.filesTotal = self.filesTotal + fileManager.size() + + return {"name":folder_name,"source":root_path,"dest":self.remote_vfs.root_path,"files":fileManager.getFiles()} + + def _dateFormat(self,dirName): + #create date_time object from foldername YYYYMMDDHHmm + date_time = datetime(int(dirName[0:4]),int(dirName[4:6]),int(dirName[6:8]),int(dirName[8:10]),int(dirName[10:12])) + + #format the string based on region settings + result = utils.getRegionalTimestamp(date_time, ['dateshort','time']) + + return result + + def _updateProgress(self,message=None): + self.filesLeft = self.filesLeft - 1 + self.progressBar.updateProgress(int((float(self.filesTotal - self.filesLeft)/float(self.filesTotal)) * 100),message) + + def _rotateBackups(self): + total_backups = int(utils.getSetting('backup_rotation')) + + if(total_backups > 0): + #get a list of valid backup folders + dirs = self.listBackups(reverse=False) + + if(len(dirs) > total_backups): + #remove backups to equal total wanted + remove_num = 0 + self.filesTotal = self.filesTotal + remove_num + 1 + + #update the progress bar if it is available + while(remove_num < (len(dirs) - total_backups) and not self.progressBar.checkCancel()): + self._updateProgress(utils.getString(30054) + " " + dirs[remove_num][1]) + utils.log("Removing backup " + dirs[remove_num][0]) + + if(dirs[remove_num][0].split('.')[-1] == 'zip'): + #this is a file, remove it that way + self.remote_vfs.rmfile(self.remote_base_path + dirs[remove_num][0]) + else: + self.remote_vfs.rmdir(self.remote_base_path + dirs[remove_num][0] + "/") + + remove_num = remove_num + 1 + + def _createValidationFile(self,dirList): + valInfo = {"name":"XBMC Backup Validation File","xbmc_version":xbmc.getInfoLabel('System.BuildVersion'),"type":0} + valDirs = [] + + for aDir in dirList: + valDirs.append({"name":aDir['name'],"path":aDir['source']}) + valInfo['directories'] = valDirs + + vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"),'w') + vFile.write(json.dumps(valInfo)) + vFile.write("") + vFile.close() + + success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val"),self.remote_vfs.root_path + "xbmcbackup.val") + + #remove the validation file + xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup.val")) + + if(success): + #android requires a .nomedia file to not index the directory as media + if(not xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + ".nomedia"))): + nmFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + ".nomedia"),'w') + nmFile.close() + + success = self.remote_vfs.put(xbmc.translatePath(utils.data_dir() + ".nomedia"),self.remote_vfs.root_path + ".nomedia") + + return success + + def _checkValidationFile(self,path): + result = None + + #copy the file and open it + self.xbmc_vfs.put(path + "xbmcbackup.val",xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val")) + + vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"),'r') + jsonString = vFile.read() + vFile.close() + + #delete after checking + xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val")) + + try: + result = json.loads(jsonString) + + if(xbmc.getInfoLabel('System.BuildVersion') != result['xbmc_version']): + shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30085),utils.getString(30086),utils.getString(30044)) + + if(not shouldContinue): + result = None + + except ValueError: + #may fail on older archives + result = None + + return result + + def _createResumeBackupFile(self): + rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'w') + rFile.write(self.restore_point) + rFile.close() + + def _readBackupConfig(self,aFile): + jFile = xbmcvfs.File(xbmc.translatePath(aFile),'r') + jsonString = jFile.read() + jFile.close() + return json.loads(jsonString) + +class FileManager: + not_dir = ['.zip','.xsp','.rar'] + exclude_dir = [] + root_dirs = [] + + def __init__(self,vfs): + self.vfs = vfs + self.fileArray = [] + self.exclude_dir = [] + self.root_dirs = [] + + def walk(self): + + for aDir in self.root_dirs: + self.addFile('-' + xbmc.translatePath(aDir['path'])) + self.walkTree(xbmc.translatePath(aDir['path']),aDir['recurse']) + + def walkTree(self,directory,recurse=True): + utils.log('walking ' + directory + ', recurse: ' + str(recurse)) + if(directory[-1:] == '/' or directory[-1:] == '\\'): + directory = directory[:-1] + + if(self.vfs.exists(directory + "/")): + dirs,files = self.vfs.listdir(directory) + + if(recurse): + #create all the subdirs first + for aDir in dirs: + dirPath = xbmc.validatePath(xbmc.translatePath(directory + "/" + aDir)) + file_ext = aDir.split('.')[-1] + + #check if directory is excluded + if(not any(dirPath.startswith(exDir) for exDir in self.exclude_dir)): + + self.addFile("-" + dirPath) + + #catch for "non directory" type files + shouldWalk = True + + for s in file_ext: + if(s in self.not_dir): + shouldWalk = False + + if(shouldWalk): + self.walkTree(dirPath) + + #copy all the files + for aFile in files: + filePath = xbmc.translatePath(directory + "/" + aFile) + self.addFile(filePath) + + def addDir(self,dirMeta): + if(dirMeta['type'] == 'include'): + self.root_dirs.append({'path':dirMeta['path'],'recurse':dirMeta['recurse']}) + else: + self.excludeFile(xbmc.translatePath(dirMeta['path'])) + + def addFile(self,filename): + try: + filename = filename.decode('UTF-8') + except UnicodeDecodeError: + filename = filename.decode('ISO-8859-2') + + #write the full remote path name of this file + utils.log("Add File: " + filename,xbmc.LOGDEBUG) + self.fileArray.append(filename) + + def excludeFile(self,filename): + try: + filename = filename.decode('UTF-8') + except UnicodeDecodeError: + filename = filename.decode('ISO-8859-2') + + #remove trailing slash + if(filename[-1] == '/' or filename[-1] == '\\'): + filename = filename[:-1] + + #write the full remote path name of this file + utils.log("Exclude File: " + filename) + self.exclude_dir.append(filename) + + def getFiles(self): + result = self.fileArray + self.fileArray = [] + self.root_dirs = [] + self.exclude_dir = [] + + return result + + def size(self): + return len(self.fileArray) diff --git a/resources/lib/dropbox/oauth.py b/resources/lib/dropbox/oauth.py index be46b40..c51f870 100644 --- a/resources/lib/dropbox/oauth.py +++ b/resources/lib/dropbox/oauth.py @@ -511,9 +511,5 @@ def _params_to_urlencoded(params): else: return str(o).encode('utf-8') - #fix for python 2.6 - utf8_params = {} - for k,v in six.iteritems(params): - utf8_params[encode(k)] = encode(v) - + utf8_params = {encode(k): encode(v) for k, v in six.iteritems(params)} return url_encode(utf8_params) diff --git a/resources/lib/dropbox/stone_serializers.py b/resources/lib/dropbox/stone_serializers.py index 3fe68b1..e4be3fa 100644 --- a/resources/lib/dropbox/stone_serializers.py +++ b/resources/lib/dropbox/stone_serializers.py @@ -237,12 +237,11 @@ class StoneToPythonPrimitiveSerializer(StoneSerializerBase): def encode_map(self, validator, value): validated_value = validator.validate(value) - #fix for python 2.6 - result = {} - for key, value in validated_value.items(): - result[self.encode_sub(validator.key_validator,key)] = self.encode_sub(validator.value_validator, value) - - return result + return { + self.encode_sub(validator.key_validator, key): + self.encode_sub(validator.value_validator, value) for + key, value in validated_value.items() + } def encode_nullable(self, validator, value): if value is None: @@ -831,12 +830,11 @@ def _decode_list( if not isinstance(obj, list): raise bv.ValidationError( 'expected list, got %s' % bv.generic_type_name(obj)) - - result = [] - for item in obj: - result.append(_json_compat_obj_decode_helper(data_type.item_validator, item, alias_validators, strict,old_style, for_msgpack)) - - return result + return [ + _json_compat_obj_decode_helper( + data_type.item_validator, item, alias_validators, strict, + old_style, for_msgpack) + for item in obj] def _decode_map( @@ -848,12 +846,15 @@ def _decode_map( if not isinstance(obj, dict): raise bv.ValidationError( 'expected dict, got %s' % bv.generic_type_name(obj)) - - result = {} - for key, value in obj.items(): - result[_json_compat_obj_decode_helper(data_type.key_validator, key, alias_validators, strict,old_style, for_msgpack)] = _json_compat_obj_decode_helper(data_type.value_validator, value, alias_validators, strict,old_style, for_msgpack) - - return result + return { + _json_compat_obj_decode_helper( + data_type.key_validator, key, alias_validators, strict, + old_style, for_msgpack): + _json_compat_obj_decode_helper( + data_type.value_validator, value, alias_validators, strict, + old_style, for_msgpack) + for key, value in obj.items() + } def _decode_nullable( diff --git a/resources/lib/dropbox/stone_validators.py b/resources/lib/dropbox/stone_validators.py index 3e9bc22..f919dc7 100644 --- a/resources/lib/dropbox/stone_validators.py +++ b/resources/lib/dropbox/stone_validators.py @@ -422,13 +422,10 @@ class Map(Composite): def validate(self, val): if not isinstance(val, dict): raise ValidationError('%r is not a valid dict' % val) - - #fix for python 2.6 - result = {} - for key, value in val.items(): - result[self.key_validator.validate(key)] = self.value_validator.validate(value) - - return result + return { + self.key_validator.validate(key): + self.value_validator.validate(value) for key, value in val.items() + } class Struct(Composite): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index f3d0993..1f4aa0e 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -14,11 +14,11 @@ def addon_dir(): def openSettings(): __Addon.openSettings() -def log(message,loglevel=xbmc.LOGNOTICE): +def log(message,loglevel=xbmc.LOGDEBUG): xbmc.log(encode(__addon_id__ + "-" + __Addon.getAddonInfo('version') + ": " + message),level=loglevel) def showNotification(message): - xbmcgui.Dialog().notification(encode(getString(30010)),encode(message),time=4000,icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/media/icon.png")) + xbmcgui.Dialog().notification(encode(getString(30010)),encode(message),time=4000,icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png")) def getSetting(name): return __Addon.getSetting(name) @@ -29,6 +29,14 @@ def setSetting(name,value): def getString(string_id): return __Addon.getLocalizedString(string_id) +def getRegionalTimestamp(date_time,dateformat=['dateshort']): + result = '' + + for aFormat in dateformat: + result = result + ("%s " % date_time.strftime(xbmc.getRegion(aFormat))) + + return result.strip() + def encode(string): result = '' diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index ae916f3..27c905b 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -1,16 +1,12 @@ import utils as utils -import tinyurl as tinyurl import xbmc import xbmcvfs import xbmcgui import zipfile -import zlib -import os import os.path import sys import dropbox from dropbox.files import WriteMode,CommitInfo,UploadSessionCursor -from pydrive.drive import GoogleDrive from authorizers import DropboxAuthorizer,GoogleDriveAuthorizer class Vfs: @@ -232,7 +228,7 @@ class DropboxFileSystem(Vfs): self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor) upload_cursor.offset = f.tell() - #if no errors we're good! + #if no errors we're good! return True except Exception as anError: utils.log(str(anError)) diff --git a/resources/settings.xml b/resources/settings.xml index 6f85c80..6867b3e 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -4,6 +4,7 @@ + @@ -13,22 +14,22 @@ - - - + + + - - - - - - - - - - - + + + + + + + + + + + diff --git a/scheduler.py b/scheduler.py index aebdd21..cf9adb1 100644 --- a/scheduler.py +++ b/scheduler.py @@ -1,191 +1,198 @@ -import xbmc -import xbmcvfs -import xbmcgui -import datetime -import time -import resources.lib.utils as utils -from resources.lib.croniter import croniter -from resources.lib.backup import XbmcBackup - -class BackupScheduler: - monitor = None - enabled = "false" - next_run = 0 - next_run_path = None - restore_point = None - - def __init__(self): - self.monitor = UpdateMonitor(update_method = self.settingsChanged) - self.enabled = utils.getSetting("enable_scheduler") - self.next_run_path = xbmc.translatePath(utils.data_dir()) + 'next_run.txt' - - if(self.enabled == "true"): - - #sleep for 2 minutes so Kodi can start and time can update correctly - xbmc.Monitor().waitForAbort(120) - - nr = 0 - if(xbmcvfs.exists(self.next_run_path)): - - fh = xbmcvfs.File(self.next_run_path) - try: - #check if we saved a run time from the last run - nr = float(fh.read()) - except ValueError: - nr = 0 - - fh.close() - - #if we missed and the user wants to play catch-up - if(0 < nr <= time.time() and utils.getSetting('schedule_miss') == 'true'): - utils.log("scheduled backup was missed, doing it now...") - progress_mode = int(utils.getSetting('progress_mode')) - - if(progress_mode == 0): - progress_mode = 1 # Kodi just started, don't block it with a foreground progress bar - - self.doScheduledBackup(progress_mode) - - self.setup() - - def setup(self): - #scheduler was turned on, find next run time - utils.log("scheduler enabled, finding next run time") - self.findNextRun(time.time()) - - def start(self): - - #check if a backup should be resumed - resumeRestore = self._resumeCheck() - - if(resumeRestore): - restore = XbmcBackup() - restore.selectRestore(self.restore_point) - #skip the advanced settings check - restore.skipAdvanced() - restore.run(XbmcBackup.Restore) - - while(not self.monitor.abortRequested()): - - if(self.enabled == "true"): - #scheduler is still on - now = time.time() - - if(self.next_run <= now): - progress_mode = int(utils.getSetting('progress_mode')) - self.doScheduledBackup(progress_mode) - - #check if we should shut the computer down - if(utils.getSetting("cron_shutdown") == 'true'): - #wait 10 seconds to make sure all backup processes and files are completed - time.sleep(10) - xbmc.executebuiltin('ShutDown()') - else: - #find the next run time like normal - self.findNextRun(now) - - xbmc.sleep(500) - - #delete monitor to free up memory - del self.monitor - - def doScheduledBackup(self,progress_mode): - if(progress_mode != 2): - utils.showNotification(utils.getString(30053)) - - backup = XbmcBackup() - - if(backup.remoteConfigured()): - - if(int(utils.getSetting('progress_mode')) in [0,1]): - backup.run(XbmcBackup.Backup,True) - else: - backup.run(XbmcBackup.Backup,False) - - #check if this is a "one-off" - if(int(utils.getSetting("schedule_interval")) == 0): - #disable the scheduler after this run - self.enabled = "false" - utils.setSetting('enable_scheduler','false') - else: - utils.showNotification(utils.getString(30045)) - - def findNextRun(self,now): - progress_mode = int(utils.getSetting('progress_mode')) - - #find the cron expression and get the next run time - cron_exp = self.parseSchedule() - - cron_ob = croniter(cron_exp,datetime.datetime.fromtimestamp(now)) - new_run_time = cron_ob.get_next(float) - - if(new_run_time != self.next_run): - self.next_run = new_run_time - utils.log("scheduler will run again on " + datetime.datetime.fromtimestamp(self.next_run).strftime('%m-%d-%Y %H:%M')) - - #write the next time to a file - fh = xbmcvfs.File(self.next_run_path, 'w') - fh.write(str(self.next_run)) - fh.close() - - #only show when not in silent mode - if(progress_mode != 2): - utils.showNotification(utils.getString(30081) + " " + datetime.datetime.fromtimestamp(self.next_run).strftime('%m-%d-%Y %H:%M')) - - def settingsChanged(self): - current_enabled = utils.getSetting("enable_scheduler") - - if(current_enabled == "true" and self.enabled == "false"): - #scheduler was just turned on - self.enabled = current_enabled - self.setup() - elif (current_enabled == "false" and self.enabled == "true"): - #schedule was turn off - self.enabled = current_enabled - - if(self.enabled == "true"): - #always recheck the next run time after an update - self.findNextRun(time.time()) - - def parseSchedule(self): - schedule_type = int(utils.getSetting("schedule_interval")) - cron_exp = utils.getSetting("cron_schedule") - - hour_of_day = utils.getSetting("schedule_time") - hour_of_day = int(hour_of_day[0:2]) - if(schedule_type == 0 or schedule_type == 1): - #every day - cron_exp = "0 " + str(hour_of_day) + " * * *" - elif(schedule_type == 2): - #once a week - day_of_week = utils.getSetting("day_of_week") - cron_exp = "0 " + str(hour_of_day) + " * * " + day_of_week - elif(schedule_type == 3): - #first day of month - cron_exp = "0 " + str(hour_of_day) + " 1 * *" - - return cron_exp - - def _resumeCheck(self): - shouldContinue = False - if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "resume.txt"))): - rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'r') - self.restore_point = rFile.read() - rFile.close() - xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "resume.txt")) - shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30042),utils.getString(30043),utils.getString(30044)) - - return shouldContinue - - -class UpdateMonitor(xbmc.Monitor): - update_method = None - - def __init__(self,*args, **kwargs): - xbmc.Monitor.__init__(self) - self.update_method = kwargs['update_method'] - - def onSettingsChanged(self): - self.update_method() - -BackupScheduler().start() +import xbmc +import xbmcvfs +import xbmcgui +from datetime import datetime +import time +import resources.lib.utils as utils +from resources.lib.croniter import croniter +from resources.lib.backup import XbmcBackup + +UPGRADE_INT = 1 #to keep track of any upgrade notifications + +class BackupScheduler: + monitor = None + enabled = "false" + next_run = 0 + next_run_path = None + restore_point = None + + def __init__(self): + self.monitor = UpdateMonitor(update_method = self.settingsChanged) + self.enabled = utils.getSetting("enable_scheduler") + self.next_run_path = xbmc.translatePath(utils.data_dir()) + 'next_run.txt' + + if(self.enabled == "true"): + + #sleep for 2 minutes so Kodi can start and time can update correctly + xbmc.Monitor().waitForAbort(120) + + nr = 0 + if(xbmcvfs.exists(self.next_run_path)): + + fh = xbmcvfs.File(self.next_run_path) + try: + #check if we saved a run time from the last run + nr = float(fh.read()) + except ValueError: + nr = 0 + + fh.close() + + #if we missed and the user wants to play catch-up + if(0 < nr <= time.time() and utils.getSetting('schedule_miss') == 'true'): + utils.log("scheduled backup was missed, doing it now...") + progress_mode = int(utils.getSetting('progress_mode')) + + if(progress_mode == 0): + progress_mode = 1 # Kodi just started, don't block it with a foreground progress bar + + self.doScheduledBackup(progress_mode) + + self.setup() + + def setup(self): + #scheduler was turned on, find next run time + utils.log("scheduler enabled, finding next run time") + self.findNextRun(time.time()) + + def start(self): + + #display upgrade messages if they exist + if(int(utils.getSetting('upgrade_notes')) < UPGRADE_INT): + xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30132)) + utils.setSetting('upgrade_notes',str(UPGRADE_INT)) + + #check if a backup should be resumed + resumeRestore = self._resumeCheck() + + if(resumeRestore): + restore = XbmcBackup() + restore.selectRestore(self.restore_point) + #skip the advanced settings check + restore.skipAdvanced() + restore.restore() + + while(not self.monitor.abortRequested()): + + if(self.enabled == "true"): + #scheduler is still on + now = time.time() + + if(self.next_run <= now): + progress_mode = int(utils.getSetting('progress_mode')) + self.doScheduledBackup(progress_mode) + + #check if we should shut the computer down + if(utils.getSetting("cron_shutdown") == 'true'): + #wait 10 seconds to make sure all backup processes and files are completed + time.sleep(10) + xbmc.executebuiltin('ShutDown()') + else: + #find the next run time like normal + self.findNextRun(now) + + xbmc.sleep(500) + + #delete monitor to free up memory + del self.monitor + + def doScheduledBackup(self,progress_mode): + if(progress_mode != 2): + utils.showNotification(utils.getString(30053)) + + backup = XbmcBackup() + + if(backup.remoteConfigured()): + + if(int(utils.getSetting('progress_mode')) in [0,1]): + backup.backup(True) + else: + backup.backup(False) + + #check if this is a "one-off" + if(int(utils.getSetting("schedule_interval")) == 0): + #disable the scheduler after this run + self.enabled = "false" + utils.setSetting('enable_scheduler','false') + else: + utils.showNotification(utils.getString(30045)) + + def findNextRun(self,now): + progress_mode = int(utils.getSetting('progress_mode')) + + #find the cron expression and get the next run time + cron_exp = self.parseSchedule() + + cron_ob = croniter(cron_exp,datetime.fromtimestamp(now)) + new_run_time = cron_ob.get_next(float) + + if(new_run_time != self.next_run): + self.next_run = new_run_time + utils.log("scheduler will run again on " + utils.getRegionalTimestamp(datetime.fromtimestamp(self.next_run),['dateshort','time'])) + + #write the next time to a file + fh = xbmcvfs.File(self.next_run_path, 'w') + fh.write(str(self.next_run)) + fh.close() + + #only show when not in silent mode + if(progress_mode != 2): + utils.showNotification(utils.getString(30081) + " " + utils.getRegionalTimestamp(datetime.fromtimestamp(self.next_run),['dateshort','time'])) + + def settingsChanged(self): + current_enabled = utils.getSetting("enable_scheduler") + + if(current_enabled == "true" and self.enabled == "false"): + #scheduler was just turned on + self.enabled = current_enabled + self.setup() + elif (current_enabled == "false" and self.enabled == "true"): + #schedule was turn off + self.enabled = current_enabled + + if(self.enabled == "true"): + #always recheck the next run time after an update + self.findNextRun(time.time()) + + def parseSchedule(self): + schedule_type = int(utils.getSetting("schedule_interval")) + cron_exp = utils.getSetting("cron_schedule") + + hour_of_day = utils.getSetting("schedule_time") + hour_of_day = int(hour_of_day[0:2]) + if(schedule_type == 0 or schedule_type == 1): + #every day + cron_exp = "0 " + str(hour_of_day) + " * * *" + elif(schedule_type == 2): + #once a week + day_of_week = utils.getSetting("day_of_week") + cron_exp = "0 " + str(hour_of_day) + " * * " + day_of_week + elif(schedule_type == 3): + #first day of month + cron_exp = "0 " + str(hour_of_day) + " 1 * *" + + return cron_exp + + def _resumeCheck(self): + shouldContinue = False + if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "resume.txt"))): + rFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"),'r') + self.restore_point = rFile.read() + rFile.close() + xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "resume.txt")) + shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30042),utils.getString(30043),utils.getString(30044)) + + return shouldContinue + + +class UpdateMonitor(xbmc.Monitor): + update_method = None + + def __init__(self,*args, **kwargs): + xbmc.Monitor.__init__(self) + self.update_method = kwargs['update_method'] + + def onSettingsChanged(self): + self.update_method() + +BackupScheduler().start()