54 Commits

Author SHA1 Message Date
Rob Weber
d7e4946d9a fix travis link 2020-12-23 15:52:05 -06:00
Rob Weber
6fdaa4f253 fix syntax error 2020-12-23 15:51:38 -06:00
Rob Weber
5f1f9fef38 version bump 2020-12-23 15:44:32 -06:00
Rob Weber
4098cd18cb updated changelog.md 2020-12-18 09:32:19 -06:00
Rob Weber
8119a09449 fix oauth import 2020-12-18 09:31:52 -06:00
Rob Weber
a0ccd85d9e part of #174, fixes ok() method def change 2020-12-18 09:28:17 -06:00
Rob
71c8d9ae54 ui settings restore upgrade
* added ability to export/save settings as json using GetSettings

* added generic copyFile method instead of duplicating

* copy and load settings file after file restore (right now only reads)

* set settings values from backup when differ than current

* store settings as part of validation file

* prompt for settings restore or set always via toggle

* unused import

* added new strings for settings restore

* updated changelog

* fix pep8 syntax

* swap setting to always prompt instead of always restore (invert)
2020-12-03 14:08:25 -06:00
Rob Weber
b470412b4f removed unused imports 2020-11-18 14:28:09 -06:00
Rob Weber
7a5886cd26 beta version, updated news 2020-11-18 14:10:22 -06:00
Rob Weber
f20887b6e7 update travis CI badge 2020-11-18 14:09:22 -06:00
Rob Weber
a0eb28a5f6 convert xbmc.translatePath to xbmcvfs.translatePath 2020-11-18 14:07:06 -06:00
Rob Weber
a198c9448a update future module 2020-11-17 19:56:15 -06:00
Rob Weber
fa3a30eb55 added flake8 pollyfill dep 2020-11-17 12:55:05 -06:00
Rob Weber
201d04afeb updated version number 2020-11-17 09:54:14 -06:00
Rob Weber
2dabb23c2d Merge branch 'matrix'
# Conflicts:
#	README.md
2020-11-17 09:52:39 -06:00
Rob Weber
2f19ec2b75 added download stats to README.md 2020-06-18 15:37:12 -05:00
Rob Weber
db215873cf updated badge path 2020-06-17 13:51:49 -05:00
Rob Weber
bd963719d4 updated badge path 2020-06-17 13:51:00 -05:00
Rob Weber
18b7f338c7 added attributes file to export-ignore 2020-06-16 15:07:16 -05:00
Rob Weber
92a9245bdc added export ignore items 2020-06-16 15:02:21 -05:00
Rob Weber
568c3758a4 add export-ignore files 2020-06-16 14:54:15 -05:00
robweber
af999f7d04 wrong rep 2020-06-15 20:56:11 -05:00
robweber
1264ab86b2 space 2020-06-15 20:48:15 -05:00
robweber
90c458d4fc updated changelog.md 2020-06-15 20:46:39 -05:00
robweber
0e6f5acfb5 added deploy script using kodi-addon-submitter 2020-06-15 20:44:41 -05:00
Wuff
c9dd381037 Version 1.6.3 - fix validatePath error (issue 166) (#167) 2020-05-20 13:01:43 -05:00
Rob Weber
55b2ac83d4 version bump 2020-04-09 14:37:09 -05:00
Rob Weber
0d14dd17c6 replace png path with jpg #165 2020-04-09 13:53:29 -05:00
Rob Weber
9fa354b467 replace PNG screenshots with JPG #165 2020-04-09 13:50:34 -05:00
Oldřich Jedlička
006485b19e Remove upper-case PNGs (#164)
Signed-off-by: Oldřich Jedlička <oldium.pro@gmail.com>
2020-02-11 15:23:52 -06:00
Rob Weber
3c2f512ecf update changelog 2019-12-31 11:03:18 -06:00
Rob Weber
190b4fd86f one more context lib 2019-12-31 11:03:11 -06:00
Rob Weber
9ecf706d63 use contextlib 2019-12-31 10:49:16 -06:00
Rob Weber
05c53b7ed8 localize advanced editor strings 2019-12-31 10:41:15 -06:00
Rob Weber
8bc73f2832 don't need this file 2019-12-31 10:33:13 -06:00
Rob Weber
edc4a7b20f updated to release version v1.6.1 2019-12-30 10:21:01 -06:00
Rob Weber
90b4aeeebe pep8 fixes 2019-12-30 10:17:58 -06:00
Rob Weber
7ce9123e1f updated changelog 2019-12-30 10:14:35 -06:00
Rob Weber
8bfef6692f Merge branch 'master' into matrix 2019-12-30 10:13:23 -06:00
Rob Weber
e63560f0c4 added a clean path function and applied it to rotate backups 2019-12-30 10:09:01 -06:00
Wuff
51f2ef3973 Fix deleting old backups on remote drives + log error (#163) 2019-12-30 09:59:03 -06:00
Rob Weber
04bac77690 moved scheduler to resources/lib and created non-complex entry point 2019-12-17 15:02:07 -06:00
Rob Weber
b1f6d36d73 updated news and version (beta2) 2019-12-10 15:24:56 -06:00
Rob
5d398836ba Show File Transfer Size (#160)
adds file transfer size to progress bar - closes #157
2019-12-10 15:16:54 -06:00
Rob Weber
23a14d67c4 Merge branch '1.6.0_fixes' into matrix 2019-12-06 13:56:06 -06:00
Rob Weber
47fcb119f3 pull build from matrix branch 2019-12-04 11:53:51 -06:00
Rob Weber
f9f49e3fe6 Merge branch 'master' into matrix 2019-12-04 11:53:12 -06:00
Rob Weber
7c23c17e33 Merge branch '1.6.0_fixes' 2019-12-04 11:49:35 -06:00
Rob Weber
8d66fa6a9f part of #159 - this will get rid of the most significant logging and keep the essentials 2019-11-27 14:24:35 -06:00
Rob Weber
5ee610a586 update getSetting calls to get ints and bools where needed 2019-11-27 14:19:41 -06:00
Rob Weber
8c4465f552 add methods for getting bools and int values directly 2019-11-27 14:19:25 -06:00
Rob Weber
3849a902ea working 16.1 beta 2019-11-27 14:05:08 -06:00
Rob Weber
4492ab593e update news 2019-11-27 13:14:42 -06:00
Rob Weber
0cc0684263 Use the dropbox module instead of bundling code - module still needs to be updated in Kodi repo 2019-11-26 12:58:35 -06:00
35 changed files with 577 additions and 422 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
.github/ export-ignore
.settings/ export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
README.md export-ignore
changelog.md export-ignore

View File

@@ -3,7 +3,7 @@ language: python
python: 3.7
install:
- pip install flake8 kodi-addon-checker
- pip install flake8 flake8_polyfill kodi-addon-checker git+https://github.com/romanvm/kodi-addon-submitter.git
before_script:
- git config core.quotepath false
@@ -11,4 +11,10 @@ before_script:
# command to run our tests
script:
- flake8 ./ --statistics --show-source --builtins=sys --ignore=E501,E722 --exclude=croniter.py # check python structure against flake8 tests, ignore long lines
- kodi-addon-checker --branch=matrix --allow-folder-id-mismatch
- kodi-addon-checker --branch=matrix --allow-folder-id-mismatch
deploy:
- provider: script
script: submit-addon -r repo-scripts -b matrix --push-branch script.xbmcbackup
on:
tags: true

View File

@@ -1,5 +1,5 @@
# Backup Addon
![Kodi Version](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Frobweber%2Fxbmcbackup%2Fmaster%2Ftrue%2Ftrue) [![Build Status](https://img.shields.io/travis/robweber/xbmcbackup/master)](https://travis-ci.org/robweber/xbmcbackup) [![License](https://img.shields.io/github/license/robweber/xbmcbackup)](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [![PEP8](https://img.shields.io/badge/code%20style-pep8-orange.svg)](https://www.python.org/dev/peps/pep-0008/)
![Kodi Version](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Fversion%2Frobweber%2Fxbmcbackup%2Fmatrix%2Ftrue%2Ftrue) ![Total Downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fweberjr.com%2Fkodi-shield%2Fdownloads%2Fmatrix%2Fscript.xbmcbackup%2F1.6.4) [![Build Status](https://img.shields.io/travis/com/robweber/xbmcbackup/matrix)](https://travis-ci.com/robweber/xbmcbackup) [![License](https://img.shields.io/github/license/robweber/xbmcbackup)](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [![PEP8](https://img.shields.io/badge/code%20style-pep8-orange.svg)](https://www.python.org/dev/peps/pep-0008/)
## About

View File

@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.xbmcbackup"
name="Backup" version="1.6.0" provider-name="robweber">
name="Backup" version="1.6.4" provider-name="robweber">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.dateutil" version="2.8.0" />
<import addon="script.module.future" version="0.16.0.4"/>
<import addon="script.module.dropbox" version="9.4.0"/>
<import addon="script.module.future" version="0.18.2+matrix.1" />
<import addon="script.module.dropbox" version="9.4.0" />
</requires>
<extension point="xbmc.python.script" library="default.py">
<provides>executable</provides>
</extension>
<extension point="xbmc.service" library="scheduler.py" />
<extension point="xbmc.python.script" library="default.py">
<provides>executable</provides>
</extension>
<extension point="xbmc.service" library="service.py" />
<extension point="xbmc.addon.metadata">
<summary lang="ar_SA">إنسخ إحتياطياً قاعده بيانات إكس بى إم سى وملفات اﻹعدادات فى حاله وقوع إنهيار مع إمكانيه اﻹسترجاع</summary>
<summary lang="be_BY">Backup and restore your Kodi database and configuration files in the event of a crash or file corruption.</summary>
@@ -84,16 +84,16 @@
<source>https://github.com/robweber/xbmcbackup</source>
<assets>
<icon>resources/images/icon.png</icon>
<screenshot>resources/images/screenshot1.png</screenshot>
<screenshot>resources/images/screenshot2.png</screenshot>
<screenshot>resources/images/screenshot3.png</screenshot>
<screenshot>resources/images/screenshot4.png</screenshot>
<screenshot>resources/images/screenshot1.jpg</screenshot>
<screenshot>resources/images/screenshot2.jpg</screenshot>
<screenshot>resources/images/screenshot3.jpg</screenshot>
<screenshot>resources/images/screenshot4.jpg</screenshot>
</assets>
<news>Version 1.6.0
- Backups/Restores now use the concept of a Set to define groups of related files. Restores can restore one set or all sets within a backup archive (no more all or nothing restores)
- Added a new Advanced Editor script for more dynamic included/excluded directories based on a JSON formatted file
- Fixed guisettings restores
- Removed GoogleDrive support - Python 3 compatibility was an issue
</news>
<news>Version 1.6.4
- updated deprecated Kodi python methods
- added better system settings/restore functionality (enabled by default)
- fixed Dropbox oauth import
- fixed xbmcgui.Dialog().ok() parameter list
</news>
</extension>
</addon>

View File

@@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Version 1.6.4](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.3...robweber:matrix-1.6.4) - 2020-12-23
### Added
- merged duplicate copy code into ```_copyFile``` method
- added method to backup/restore Kodi settings via the GetSettings/SetSettingValue JSON methods in the validation file
- added setting to always restore settings or prompt at the time of backup
### Changed
- updated script.module.future version to current
- swapped xbmc.translatePath for xbmcvfs.translatePath, deprecated
### Fixed
- fixed calls to ```xbmcgui.Dialog().ok()```, method definition changed to only allow one message arg with Kodi 19
- fixed import of dropbox Oauth package in authorizer flow
### Removed
- removed old xml GuiSettings parsing for settings restore
## [Version 1.6.3](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.2...robweber:matrix-1.6.3) - 2020-06-15
### Changed
- fixed validatePath error (issue #166) thanks (thanks @AnonTester)
## [Version 1.6.2](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.1...robweber:matrix-1.6.2) - 2019-04-09
### Changed
- changed PNG screenshots to JPG (per [#165](https://github.com/robweber/xbmcbackup/issues/165))
## [Version 1.6.1](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.0...robweber:matrix-1.6.1) - 2019-12-30
### Added
- added method to get size of a file from the VFS
- added total transfer size information to progress bar with appropriate precision (KB, MB, etc)
- show file size of zip files in the restore selection dialog
- added getSettingInt and getSettingBool to utils.py class
- added verbose logging setting and tied it to logging related to file paths added/written, this will significantly reduce the debug log size (thanks CastagnaIT)
- localize advanced editor strings instead of hard coding English
### Changed
- display every file transfered in progress bar, not just directory
- base progress bar percent on transfer size, not total files
- changed getSettings where needed to getSettingBool and getSettingInt
- use service.py to start scheduler, moving scheduler to resources/lib/scheduler.py Kodi doesn't cache files in the root directory
- fixed issues with rotating backups where trailing slash was missing (thanks @AnonTester)
- read/write files using contextlib
## [Version 1.6.0](https://github.com/robweber/xbmcbackup/compare/krypton-1.5.2...robweber:matrix-1.6.0) - 2019-11-26
### Added
@@ -16,7 +70,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- addon.xml updated to use Leia specific syntax and library imports
- removed specific encode() calls per Python2/3 compatibility
- call isdigit() method on the string directly instead of str.isdigit() (results in unicode error)
- added flake8 testing to travis-ci
- added flake8 testing to travis-ci
- updated code to make python3 compatible
- updated code for pep9 styling
- use setArt() to set ListItem icons as the icon= constructor is deprecated
@@ -34,16 +88,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Updated Changelog format to the one suggested by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added script.module.dropbox import as a dependency for Dropbox filesystem
### Changed
- Fixed issue getting xbmcbackup.val file from non-zipped remote directories. Was being copied as though it was a local file so it was failing.
- Fixed issue getting xbmcbackup.val file from non-zipped remote directories. Was being copied as though it was a local file so it was failing.
- Use linux path separator (/) all the time, Kodi will interpret this correctly on windows. Was causing issues with remote file systems since os.path.sep
- Fixed minor python code style changes based on kodi-addon-checker output
- Fixed minor python code style changes based on kodi-addon-checker output
### Removed
- files releated to dropbox library, using script.module.dropbox import now
- files releated to dropbox library, using script.module.dropbox import now
## Version 1.5.1 - 2019-09-10
@@ -53,7 +107,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 1.5.0 - 2019-08-26
### Added
- Added new Advanced file editor and file selection based on a .json
- Added new Advanced file editor and file selection based on a .json
### Removed
- File backups and restores will not work with old version - breaking change with previous versions PR117
@@ -63,7 +117,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added file chunk support for Dropbox uploads
- added scheduler delay to assist with time sync (rpi mostly), will delay startup by 2 min
### Changed
- fixed settings duplicate ids, thanks aster-anto
@@ -75,7 +129,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 1.1.1
### Added
- added ability to "catchup" on missed scheduled backup
- added ability to "catchup" on missed scheduled backup
### Changed
- fixed error on authorizers (missing secret/key)
@@ -86,10 +140,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added tinyurl generation for oauth urls
### Changed
- moved authorize to settings area for cloud storage
## Version 1.0.9
### Changed
@@ -110,7 +164,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- added progress for zip extraction - hopefully helps with extract errors
### Changed
- fix for custom directories not working recursively
@@ -250,7 +304,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## Version 0.3.9
- added "just once" scheduler for one-off type backups
- added "just once" scheduler for one-off type backups
- show notification on scheduler
- update updated language files from Transifex

View File

@@ -38,7 +38,7 @@ if(mode == -1):
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):
if(utils.getSettingInt('backup_selection_type') == 1):
options.append(utils.getString(30125))
# figure out if this is a backup or a restore from the user
@@ -52,7 +52,7 @@ if(mode != -1):
if(mode == 2):
# open the settings dialog
utils.openSettings()
elif(mode == 3 and int(utils.getSetting('backup_selection_type')) == 1):
elif(mode == 3 and utils.getSettingInt('backup_selection_type') == 1):
# open the advanced editor
xbmc.executebuiltin('RunScript(special://home/addons/script.xbmcbackup/launcher.py, action=advanced_editor)')
elif(backup.remoteConfigured()):

View File

@@ -1,6 +1,5 @@
# launcher for various helpful functions found in the settings.xml area
import sys
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.utils as utils
@@ -14,9 +13,9 @@ def authorize_cloud(cloudProvider):
authorizer = DropboxAuthorizer()
if(authorizer.authorize()):
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30027) + ' ' + utils.getString(30106))
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s' % (utils.getString(30027), utils.getString(30106)))
else:
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30107) + ' ' + utils.getString(30027))
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s' % (utils.getString(30107), utils.getString(30027)))
def remove_auth():
@@ -25,8 +24,8 @@ def remove_auth():
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
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt")) # dropbox
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "google_drive.dat")) # google drive
def get_params():

View File

@@ -1,2 +0,0 @@
*.pyo

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -555,4 +555,40 @@ msgstr ""
msgctxt "#30141"
msgid "This will erase any current Advanced Editor settings"
msgstr ""
msgstr ""
msgctxt "#30142"
msgid "Enable Verbose Logging"
msgstr ""
msgctxt "#30143"
msgid "Exclude a specific folder from this backup set"
msgstr ""
msgctxt "#30144"
msgid "Include a specific folder to this backup set"
msgstr ""
msgctxt "#30145"
msgid "Type"
msgstr ""
msgctxt "#30146"
msgid "Include Sub Folders"
msgstr ""
msgctxt "#30147"
msgid "Toggle Sub Folders"
msgstr ""
msgctxt "#30148"
msgid "Ask before restoring Kodi UI settings"
msgstr ""
msgctxt "#30149"
msgid "Restore Kodi UI Settings"
msgstr ""
msgctxt "#30150"
msgid "Restore saved Kodi system settings from backup?"
msgstr ""

View File

@@ -1,5 +1,4 @@
import json
import xbmc
import xbmcgui
import xbmcvfs
import os.path
@@ -7,7 +6,7 @@ from . import utils as utils
class BackupSetManager:
jsonFile = xbmc.translatePath(utils.data_dir() + "custom_paths.json")
jsonFile = xbmcvfs.translatePath(utils.data_dir() + "custom_paths.json")
paths = None
def __init__(self):
@@ -106,7 +105,7 @@ class AdvancedBackupEditor:
rootFolder = rootFolder + '/'
# check that this path even exists
if(not xbmcvfs.exists(xbmc.translatePath(rootFolder))):
if(not xbmcvfs.exists(xbmcvfs.translatePath(rootFolder))):
self.dialog.ok(utils.getString(30117), utils.getString(30118), rootFolder)
return None
else:
@@ -122,13 +121,13 @@ class AdvancedBackupEditor:
rootPath = backupSet['root']
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))]
options = [xbmcgui.ListItem(utils.getString(30120), utils.getString(30143)), xbmcgui.ListItem(utils.getString(30135), utils.getString(30144)), 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))))
options.append(xbmcgui.ListItem(self._cleanPath(rootPath, aDir['path']), "%s: %s" % (utils.getString(30145), 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']))))
options.append(xbmcgui.ListItem(self._cleanPath(rootPath, aDir['path']), "%s: %s | %s: %s" % (utils.getString(30145), utils.getString(30134), utils.getString(30146), str(aDir['recurse']))))
optionSelected = self.dialog.select(utils.getString(30122) + ' ' + name, options, useDetails=True)
@@ -157,7 +156,7 @@ class AdvancedBackupEditor:
cOptions = ['Delete']
if(backupSet['dirs'][optionSelected - 3]['type'] == 'include'):
cOptions.append('Toggle Sub Folders')
cOptions.append(utils.getString(30147))
contextOption = self.dialog.contextmenu(cOptions)
@@ -227,7 +226,7 @@ class AdvancedBackupEditor:
shouldContinue = self.dialog.yesno(utils.getString(30139), utils.getString(30140), utils.getString(30141))
if(shouldContinue):
source = xbmc.translatePath(os.path.join(utils.addon_dir(), 'resources', 'data', 'default_files.json'))
dest = xbmc.translatePath(os.path.join(utils.data_dir(), 'custom_paths.json'))
source = xbmcvfs.translatePath(os.path.join(utils.addon_dir(), 'resources', 'data', 'default_files.json'))
dest = xbmcvfs.translatePath(os.path.join(utils.data_dir(), 'custom_paths.json'))
xbmcvfs.copy(source, dest)

View File

@@ -1,4 +1,3 @@
import xbmc
import xbmcgui
import xbmcvfs
import resources.lib.tinyurl as tinyurl
@@ -7,6 +6,7 @@ import resources.lib.utils as utils
# don't die on import error yet, these might not even get used
try:
from dropbox import dropbox
from dropbox import oauth
except ImportError:
pass
@@ -24,7 +24,7 @@ class DropboxAuthorizer:
if(self.APP_KEY == '' and self.APP_SECRET == ''):
# we can't go any farther, need these for sure
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30027) + ' ' + utils.getString(30058), utils.getString(30059))
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s\n%s' % (utils.getString(30027), utils.getString(30058), utils.getString(30059)))
result = False
@@ -46,13 +46,13 @@ class DropboxAuthorizer:
self._deleteToken()
# copied flow from http://dropbox-sdk-python.readthedocs.io/en/latest/moduledoc.html#dropbox.oauth.DropboxOAuth2FlowNoRedirect
flow = dropbox.oauth.DropboxOAuth2FlowNoRedirect(self.APP_KEY, self.APP_SECRET)
flow = oauth.DropboxOAuth2FlowNoRedirect(self.APP_KEY, self.APP_SECRET)
url = flow.start()
# print url in log
utils.log("Authorize URL: " + url)
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30056), utils.getString(30057), tinyurl.shorten(url))
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s\n%s' % (utils.getString(30056), utils.getString(30057), str(tinyurl.shorten(url), 'utf-8')))
# get the auth code
code = xbmcgui.Dialog().input(utils.getString(30027) + ' ' + utils.getString(30103))
@@ -89,14 +89,14 @@ class DropboxAuthorizer:
def _setToken(self, token):
# write the token files
token_file = open(xbmc.translatePath(utils.data_dir() + "tokens.txt"), 'w')
token_file = open(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"), 'w')
token_file.write(token)
token_file.close()
def _getToken(self):
# get token, if it exists
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "tokens.txt"))):
token_file = open(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))):
token_file = open(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))
token = token_file.read()
token_file.close()
@@ -105,5 +105,5 @@ class DropboxAuthorizer:
return ""
def _deleteToken(self):
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "tokens.txt"))):
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))):
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))

View File

@@ -4,6 +4,7 @@ import json
import xbmc
import xbmcgui
import xbmcvfs
import os.path
from . import utils as utils
from datetime import datetime
from . vfs import XBMCFileSystem, DropboxFileSystem, ZipFileSystem
@@ -39,14 +40,14 @@ class XbmcBackup:
# for the progress bar
progressBar = None
filesLeft = 0
filesTotal = 1
transferSize = 0
transferLeft = 0
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.xbmc_vfs = XBMCFileSystem(xbmcvfs.translatePath('special://home'))
self.configureRemote()
utils.log(utils.getString(30046))
@@ -92,8 +93,8 @@ class XbmcBackup:
if(file_ext == 'zip' and len(folderName) == 12 and folderName.isdigit()):
# format the name according to regional settings
folderName = self._dateFormat(folderName)
# format the name according to regional settings and display the file size
folderName = "%s - %s" % (self._dateFormat(folderName), utils.diskString(self.remote_vfs.fileSize(self.remote_base_path + aFile)))
result.append((aFile, folderName))
@@ -124,14 +125,14 @@ class XbmcBackup:
utils.log('File Selection Type: ' + str(utils.getSetting('backup_selection_type')))
allFiles = []
if(int(utils.getSetting('backup_selection_type')) == 0):
if(utils.getSettingInt('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'):
if(utils.getSettingBool('backup_' + aDir)):
# get a file listing and append it to the allfiles array
allFiles.append(self._addBackupDir(aDir, selectedDirs[aDir]['root'], selectedDirs[aDir]['dirs']))
else:
@@ -162,9 +163,9 @@ class XbmcBackup:
orig_base_path = self.remote_vfs.root_path
# backup all the files
self.filesLeft = self.filesTotal
self.transferLeft = self.transferSize
for fileGroup in allFiles:
self.xbmc_vfs.set_root(xbmc.translatePath(fileGroup['source']))
self.xbmc_vfs.set_root(xbmcvfs.translatePath(fileGroup['source']))
self.remote_vfs.set_root(fileGroup['dest'] + fileGroup['name'])
filesCopied = self._copyFiles(fileGroup['files'], self.xbmc_vfs, self.remote_vfs)
@@ -176,28 +177,30 @@ class XbmcBackup:
self.xbmc_vfs.set_root("special://home/")
self.remote_vfs.set_root(orig_base_path)
if(utils.getSetting("compress_backups") == 'true'):
if(utils.getSettingBool("compress_backups")):
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.xbmc_vfs.rename(xbmcvfs.translatePath("special://temp/xbmc_backup_temp.zip"), xbmcvfs.translatePath("special://temp/" + zip_name))
fileManager.addFile(xbmcvfs.translatePath("special://temp/" + zip_name))
# set root to data dir home and reset remote
self.xbmc_vfs.set_root(xbmcvfs.translatePath("special://temp/"))
self.remote_vfs = self.saved_remote_vfs
self.progressBar.updateProgress(98, utils.getString(30088))
# update the amount to transfer
self.transferSize = fileManager.fileSize()
self.transferLeft = self.transferSize
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))
shouldContinue = xbmcgui.Dialog().ok(utils.getString(30089), '%s\n%s' % (utils.getString(30090), utils.getString(30091)))
# delete the temp zip file
self.xbmc_vfs.rmfile(xbmc.translatePath("special://temp/" + zip_name))
self.xbmc_vfs.rmfile(xbmcvfs.translatePath("special://temp/" + zip_name))
# remove old backups
self._rotateBackups()
@@ -217,25 +220,28 @@ class XbmcBackup:
utils.log("copying zip file: " + self.restore_point)
# set root to data dir home
self.xbmc_vfs.set_root(xbmc.translatePath("special://temp/"))
self.xbmc_vfs.set_root(xbmcvfs.translatePath("special://temp/"))
if(not self.xbmc_vfs.exists(xbmc.translatePath("special://temp/" + self.restore_point))):
if(not self.xbmc_vfs.exists(xbmcvfs.translatePath("special://temp/" + self.restore_point))):
# copy just this file from the remote vfs
self.transferSize = self.remote_vfs.fileSize(self.remote_base_path + self.restore_point)
zipFile = []
zipFile.append(self.remote_base_path + self.restore_point)
zipFile.append({'file': self.remote_base_path + self.restore_point, 'size': self.transferSize})
# set transfer size
self.transferLeft = self.transferSize
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')
zip_vfs = ZipFileSystem(xbmcvfs.translatePath("special://temp/" + self.restore_point), 'r')
extractor = ZipExtractor()
if(not extractor.extract(zip_vfs, xbmc.translatePath("special://temp/"), self.progressBar)):
if(not extractor.extract(zip_vfs, xbmcvfs.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))
self.xbmc_vfs.rmfile(xbmcvfs.translatePath("special://temp/" + self.restore_point))
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30101))
return
@@ -244,12 +250,12 @@ class XbmcBackup:
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/"))
self.remote_vfs = XBMCFileSystem(xbmcvfs.translatePath("special://temp/" + self.restore_point.split(".")[0] + "/"))
self.xbmc_vfs.set_root(xbmcvfs.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)
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (utils.getString(30045), self.remote_vfs.root_path))
return
valFile = self._checkValidationFile(self.remote_vfs.root_path)
@@ -278,6 +284,12 @@ class XbmcBackup:
xbmcgui.Dialog().ok(utils.getString(30077), utils.getString(30078))
return
# check if settings should be restored from this backup
restoreSettings = not utils.getSettingBool('always_prompt_restore_settings')
if(not restoreSettings and 'system_settings' in valFile):
# prompt the user to restore settings yes/no
restoreSettings = xbmcgui.Dialog().yesno(utils.getString(30149), utils.getString(30150))
# use a multiselect dialog to select sets to restore
restoreSets = [n['name'] for n in valFile['directories']]
@@ -288,40 +300,44 @@ class XbmcBackup:
selectedSets = [restoreSets.index(n) for n in selectedSets if n in restoreSets] # if set name not found just skip it
if(selectedSets is not 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']))
self.xbmc_vfs.set_root(xbmcvfs.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()
self.transferSize = self.transferSize + fileManager.fileSize()
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'])
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (utils.getString(30045), self.remote_vfs.root_path + aDir['name']))
# restore all the files
self.filesLeft = self.filesTotal
self.transferLeft = self.transferSize
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)
# update the Kodi settings - if we can
if('system_settings' in valFile and restoreSettings):
self.progressBar.updateProgress(98, "Restoring Kodi settings")
gui_settings = GuiSettingsManager()
gui_settings.restore(valFile['system_settings'])
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.rmfile(xbmcvfs.translatePath("special://temp/" + self.restore_point))
self.xbmc_vfs.rmdir(self.remote_vfs.root_path)
# update the guisettings information (or what we can from it)
gui_settings = GuiSettingsManager()
gui_settings.run()
# call update addons to refresh everything
xbmc.executebuiltin('UpdateLocalAddons')
@@ -333,17 +349,17 @@ class XbmcBackup:
# append backup folder name
progressBarTitle = utils.getString(30010) + " - "
if(mode == self.Backup and self.remote_vfs.root_path != ''):
if(utils.getSetting("compress_backups") == 'true'):
if(utils.getSettingBool("compress_backups")):
# 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'))):
if(self.xbmc_vfs.exists(xbmcvfs.translatePath('special://temp/xbmc_backup_temp.zip'))):
if(not self.xbmc_vfs.rmfile(xbmcvfs.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))
xbmcgui.Dialog().ok(utils.getString(30010), '%s\n%s' % (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 = ZipFileSystem(xbmcvfs.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)
@@ -379,7 +395,7 @@ class XbmcBackup:
result = True
utils.log("Source: " + source.root_path)
utils.log("Desintation: " + dest.root_path)
utils.log("Destination: " + 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)):
@@ -387,21 +403,18 @@ class XbmcBackup:
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()
if(utils.getSettingBool('verbose_logging')):
utils.log('Writing file: ' + aFile['file'])
wroteFile = True
destFile = dest.root_path + aFile[len(source.root_path):]
if(isinstance(source, DropboxFileSystem)):
# if copying from cloud storage we need the file handle, use get_file
wroteFile = source.get_file(aFile, destFile)
else:
# copy using normal method
wroteFile = dest.put(aFile, destFile)
if(aFile['file'].startswith("-")):
self._updateProgress('%s remaining, writing %s' % (utils.diskString(self.transferLeft), os.path.basename(aFile['file'][len(source.root_path):]) + "/"))
dest.mkdir(dest.root_path + aFile['file'][len(source.root_path) + 1:])
else:
self._updateProgress('%s remaining, writing %s' % (utils.diskString(self.transferLeft), os.path.basename(aFile['file'][len(source.root_path):])))
self.transferLeft = self.transferLeft - aFile['size']
# copy the file
wroteFile = self._copyFile(source, dest, aFile['file'], dest.root_path + aFile['file'][len(source.root_path):])
# if result is still true but this file failed
if(not wroteFile and result):
@@ -409,18 +422,31 @@ class XbmcBackup:
return result
def _copyFile(self, source, dest, sourceFile, destFile):
result = True
if(isinstance(source, DropboxFileSystem)):
# if copying from cloud storage we need the file handle, use get_file
result = source.get_file(sourceFile, destFile)
else:
# copy using normal method
result = dest.put(sourceFile, destFile)
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))
self.xbmc_vfs.set_root(xbmcvfs.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()
# update total size
self.transferSize = self.transferSize + fileManager.fileSize()
return {"name": folder_name, "source": root_path, "dest": self.remote_vfs.root_path, "files": fileManager.getFiles()}
@@ -434,11 +460,10 @@ class XbmcBackup:
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)
self.progressBar.updateProgress(int((float(self.transferSize - self.transferLeft) / float(self.transferSize)) * 100), message)
def _rotateBackups(self):
total_backups = int(utils.getSetting('backup_rotation'))
total_backups = utils.getSettingInt('backup_rotation')
if(total_backups > 0):
# get a list of valid backup folders
@@ -447,7 +472,6 @@ class XbmcBackup:
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()):
@@ -456,37 +480,42 @@ class XbmcBackup:
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])
self.remote_vfs.rmfile(self.remote_vfs.clean_path(self.remote_base_path) + dirs[remove_num][0])
else:
self.remote_vfs.rmdir(self.remote_base_path + dirs[remove_num][0] + "/")
self.remote_vfs.rmdir(self.remote_vfs.clean_path(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}
valInfo = {"name": "XBMC Backup Validation File", "xbmc_version": xbmc.getInfoLabel('System.BuildVersion'), "type": 0, "system_settings": []}
valDirs = []
# save list of file sets
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')
# dump all current Kodi settings
gui_settings = GuiSettingsManager()
valInfo['system_settings'] = gui_settings.backup()
vFile = xbmcvfs.File(xbmcvfs.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")
success = self._copyFile(self.xbmc_vfs, self.remote_vfs, xbmcvfs.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"))
xbmcvfs.delete(xbmcvfs.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')
if(not xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + ".nomedia"))):
nmFile = xbmcvfs.File(xbmcvfs.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")
success = self._copyFile(self.xbmc_vfs, self.remote_vfs, xbmcvfs.translatePath(utils.data_dir() + ".nomedia"), self.remote_vfs.root_path + ".nomedia")
return success
@@ -494,17 +523,13 @@ class XbmcBackup:
result = None
# copy the file and open it
if(isinstance(self.remote_vfs, DropboxFileSystem)):
self.remote_vfs.get_file(path + "xbmcbackup.val", xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
else:
self.xbmc_vfs.put(path + "xbmcbackup.val", xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
self._copyFile(self.remote_vfs, self.xbmc_vfs, path + "xbmcbackup.val", xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
vFile = xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"), 'r')
jsonString = vFile.read()
vFile.close()
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"), 'r') as vFile:
jsonString = vFile.read()
# delete after checking
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
try:
result = json.loads(jsonString)
@@ -522,11 +547,11 @@ class XbmcBackup:
return result
def _createResumeBackupFile(self):
with xbmcvfs.File(xbmc.translatePath(utils.data_dir() + "resume.txt"), 'w') as f:
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "resume.txt"), 'w') as f:
f.write(self.restore_point)
def _readBackupConfig(self, aFile):
with xbmcvfs.File(xbmc.translatePath(aFile), 'r') as f:
with xbmcvfs.File(xbmcvfs.translatePath(aFile), 'r') as f:
jsonString = f.read()
return json.loads(jsonString)
@@ -536,6 +561,7 @@ class FileManager:
exclude_dir = []
root_dirs = []
pathSep = '/'
totalSize = 0
def __init__(self, vfs):
self.vfs = vfs
@@ -546,11 +572,13 @@ class FileManager:
def walk(self):
for aDir in self.root_dirs:
self.addFile('-' + xbmc.translatePath(aDir['path']))
self.walkTree(xbmc.translatePath(aDir['path']), aDir['recurse'])
self.addFile('-' + xbmcvfs.translatePath(aDir['path']))
self.walkTree(xbmcvfs.translatePath(aDir['path']), aDir['recurse'])
def walkTree(self, directory, recurse=True):
utils.log('walking ' + directory + ', recurse: ' + str(recurse))
if(utils.getSettingBool('verbose_logging')):
utils.log('walking ' + directory + ', recurse: ' + str(recurse))
if(directory[-1:] == '/' or directory[-1:] == '\\'):
directory = directory[:-1]
@@ -560,7 +588,7 @@ class FileManager:
if(recurse):
# create all the subdirs first
for aDir in dirs:
dirPath = xbmc.validatePath(xbmc.translatePath(directory + self.pathSep + aDir))
dirPath = xbmcvfs.validatePath(xbmcvfs.translatePath(directory + self.pathSep + aDir))
file_ext = aDir.split('.')[-1]
# check if directory is excluded
@@ -580,22 +608,27 @@ class FileManager:
# copy all the files
for aFile in files:
filePath = xbmc.translatePath(directory + self.pathSep + aFile)
filePath = xbmcvfs.translatePath(directory + self.pathSep + 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']))
self.excludeFile(xbmcvfs.translatePath(dirMeta['path']))
def addFile(self, filename):
# write the full remote path name of this file
utils.log("Add File: " + filename)
self.fileArray.append(filename)
if(utils.getSettingBool('verbose_logging')):
utils.log("Add File: " + filename)
# get the file size
fSize = self.vfs.fileSize(filename)
self.totalSize = self.totalSize + fSize
self.fileArray.append({'file': filename, 'size': fSize})
def excludeFile(self, filename):
# remove trailing slash
if(filename[-1] == '/' or filename[-1] == '\\'):
filename = filename[:-1]
@@ -609,8 +642,12 @@ class FileManager:
self.fileArray = []
self.root_dirs = []
self.exclude_dir = []
self.totalSize = 0
return result
def size(self):
def totalFiles(self):
return len(self.fileArray)
def fileSize(self):
return self.totalSize

View File

@@ -1,72 +1,47 @@
import json
import xbmc
import xbmcvfs
from . import utils as utils
from xml.dom import minidom
from xml.parsers.expat import ExpatError
class GuiSettingsManager:
doc = None
filename = 'kodi_settings.json'
systemSettings = None
def __init__(self):
# first make a copy of the file
xbmcvfs.copy(xbmc.translatePath('special://home/userdata/guisettings.xml'), xbmc.translatePath("special://home/userdata/guisettings.xml.restored"))
# get all of the current Kodi settings
json_response = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings","params":{"level":"expert"}}'))
# read in the copy
self._readFile(xbmc.translatePath('special://home/userdata/guisettings.xml.restored'))
self.systemSettings = json_response['result']['settings']
def run(self):
# get a list of all the settings we can manipulate via json
json_response = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings","params":{"level":"advanced"}}'))
def backup(self):
utils.log('Backing up Kodi settings')
settings = json_response['result']['settings']
currentSettings = {}
# return all current settings
return self.systemSettings
for aSetting in settings:
if('value' in aSetting):
currentSettings[aSetting['id']] = aSetting['value']
def restore(self, restoreSettings):
utils.log('Restoring Kodi settings')
# parse the existing xml file and get all the settings we need to restore
restoreSettings = self.__parseNodes(self.doc.getElementsByTagName('setting'))
updateJson = {"jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": {"setting": "", "value": ""}}
# get a list where the restore setting value != the current value
updateSettings = {k: v for k, v in list(restoreSettings.items()) if (k in currentSettings and currentSettings[k] != v)}
# create a setting=value dict of the current settings
settingsDict = {}
for aSetting in self.systemSettings:
# ignore action types, no value
if(aSetting['type'] != 'action'):
settingsDict[aSetting['id']] = aSetting['value']
# go through all the found settings and update them
jsonObj = {"jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": {"setting": "", "value": ""}}
for anId, aValue in list(updateSettings.items()):
utils.log("updating: " + anId + ", value: " + str(aValue))
restoreCount = 0
for aSetting in restoreSettings:
# only update a setting if its different than the current (action types have no value)
if(aSetting['type'] != 'action' and settingsDict[aSetting['id']] != aSetting['value']):
if(utils.getSettingBool('verbose_logging')):
utils.log('%s different than current: %s' % (aSetting['id'], str(aSetting['value'])))
jsonObj['params']['setting'] = anId
jsonObj['params']['value'] = aValue
updateJson['params']['setting'] = aSetting['id']
updateJson['params']['value'] = aSetting['value']
xbmc.executeJSONRPC(json.dumps(jsonObj))
xbmc.executeJSONRPC(json.dumps(updateJson))
restoreCount = restoreCount + 1
def __parseNodes(self, nodeList):
result = {}
for node in nodeList:
nodeValue = ''
if(node.firstChild is not None):
nodeValue = node.firstChild.nodeValue
# check for numbers and booleans
if(nodeValue.isdigit()):
nodeValue = int(nodeValue)
elif(nodeValue == 'true'):
nodeValue = True
elif(nodeValue == 'false'):
nodeValue = False
result[node.getAttribute('id')] = nodeValue
return result
def _readFile(self, fileLoc):
if(xbmcvfs.exists(fileLoc)):
try:
self.doc = minidom.parse(fileLoc)
except ExpatError:
utils.log("Can't read " + fileLoc)
utils.log('Update %d settings' % restoreCount)

View File

@@ -15,9 +15,9 @@ class BackupProgressBar:
self.override = progressOverride
# check if we should use the progress bar
if(int(utils.getSetting('progress_mode')) != 2):
if(utils.getSettingInt('progress_mode') != 2):
# check if background or normal
if(int(utils.getSetting('progress_mode')) == 0 and not self.override):
if(utils.getSettingInt('progress_mode') == 0 and not self.override):
self.mode = self.DIALOG
self.progressBar = xbmcgui.DialogProgress()
else:

View File

@@ -1,200 +1,194 @@
import time
from datetime import datetime
import xbmc
import xbmcvfs
import xbmcgui
import resources.lib.utils as utils
from resources.lib.croniter import croniter
from resources.lib.backup import XbmcBackup
UPGRADE_INT = 2 # 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()
import time
from datetime import datetime
import xbmc
import xbmcvfs
import xbmcgui
from . import utils as utils
from resources.lib.croniter import croniter
from resources.lib.backup import XbmcBackup
UPGRADE_INT = 2 # 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.getSettingBool("enable_scheduler")
self.next_run_path = xbmcvfs.translatePath(utils.data_dir()) + 'next_run.txt'
if(self.enabled):
# 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)):
with xbmcvfs.File(self.next_run_path) as fh:
try:
# check if we saved a run time from the last run
nr = float(fh.read())
except ValueError:
nr = 0
# if we missed and the user wants to play catch-up
if(0 < nr <= time.time() and utils.getSettingBool('schedule_miss')):
utils.log("scheduled backup was missed, doing it now...")
progress_mode = utils.getSettingInt('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(utils.getSettingInt('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):
# scheduler is still on
now = time.time()
if(self.next_run <= now):
progress_mode = utils.getSettingInt('progress_mode')
self.doScheduledBackup(progress_mode)
# check if we should shut the computer down
if(utils.getSettingBool("cron_shutdown")):
# 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(utils.getSettingInt('progress_mode') in [0, 1]):
backup.backup(True)
else:
backup.backup(False)
# check if this is a "one-off"
if(utils.getSettingInt("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 = utils.getSettingInt('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
with xbmcvfs.File(self.next_run_path, 'w') as fh:
fh.write(str(self.next_run))
# 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.getSettingBool("enable_scheduler")
if(current_enabled and not self.enabled):
# scheduler was just turned on
self.enabled = current_enabled
self.setup()
elif (not current_enabled and self.enabled):
# schedule was turn off
self.enabled = current_enabled
if(self.enabled):
# always recheck the next run time after an update
self.findNextRun(time.time())
def parseSchedule(self):
schedule_type = utils.getSettingInt("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(xbmcvfs.translatePath(utils.data_dir() + "resume.txt"))):
rFile = xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "resume.txt"), 'r')
self.restore_point = rFile.read()
rFile.close()
xbmcvfs.delete(xbmcvfs.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()

View File

@@ -1,6 +1,7 @@
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
__addon_id__ = 'script.xbmcbackup'
__Addon = xbmcaddon.Addon(__addon_id__)
@@ -23,13 +24,21 @@ def log(message, loglevel=xbmc.LOGDEBUG):
def showNotification(message):
xbmcgui.Dialog().notification(getString(30010), message, time=4000, icon=xbmc.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png"))
xbmcgui.Dialog().notification(getString(30010), message, time=4000, icon=xbmcvfs.translatePath(__Addon.getAddonInfo('path') + "/resources/images/icon.png"))
def getSetting(name):
return __Addon.getSetting(name)
def getSettingBool(name):
return bool(__Addon.getSettingBool(name))
def getSettingInt(name):
return __Addon.getSettingInt(name)
def setSetting(name, value):
__Addon.setSetting(name, value)
@@ -45,3 +54,16 @@ def getRegionalTimestamp(date_time, dateformat=['dateshort']):
result = result + ("%s " % date_time.strftime(xbmc.getRegion(aFormat)))
return result.strip()
def diskString(fSize):
# convert a size in kilobytes to the best possible match and return as a string
fSize = float(fSize)
i = 0
sizeNames = ['KB', 'MB', 'GB', 'TB']
while(fSize > 1024):
fSize = fSize / 1024
i = i + 1
return "%0.2f%s" % (fSize, sizeNames[i])

View File

@@ -2,7 +2,6 @@ from __future__ import unicode_literals
import zipfile
import os.path
import sys
import xbmc
import xbmcvfs
import xbmcgui
from dropbox import dropbox
@@ -17,16 +16,19 @@ class Vfs:
def __init__(self, rootString):
self.set_root(rootString)
def set_root(self, rootString):
old_root = self.root_path
self.root_path = rootString
def clean_path(self, path):
# fix slashes
self.root_path = self.root_path.replace("\\", "/")
path = path.replace("\\", "/")
# check if trailing slash is included
if(self.root_path[-1:] != "/"):
self.root_path = self.root_path + "/"
if(path[-1:] != '/'):
path = path + '/'
return path
def set_root(self, rootString):
old_root = self.root_path
self.root_path = self.clean_path(rootString)
# return the old root
return old_root
@@ -55,6 +57,9 @@ class Vfs:
def cleanup(self):
return True
def fileSize(self, filename):
return 0 # result should be in KB
class XBMCFileSystem(Vfs):
@@ -62,10 +67,10 @@ class XBMCFileSystem(Vfs):
return xbmcvfs.listdir(directory)
def mkdir(self, directory):
return xbmcvfs.mkdir(xbmc.translatePath(directory))
return xbmcvfs.mkdir(xbmcvfs.translatePath(directory))
def put(self, source, dest):
return xbmcvfs.copy(xbmc.translatePath(source), xbmc.translatePath(dest))
return xbmcvfs.copy(xbmcvfs.translatePath(source), xbmcvfs.translatePath(dest))
def rmdir(self, directory):
return xbmcvfs.rmdir(directory, True)
@@ -79,6 +84,12 @@ class XBMCFileSystem(Vfs):
def exists(self, aFile):
return xbmcvfs.exists(aFile)
def fileSize(self, filename):
with xbmcvfs.File(filename) as f:
result = f.size() / 1024 # bytes to kilobytes
return result
class ZipFileSystem(Vfs):
zip = None
@@ -96,7 +107,7 @@ class ZipFileSystem(Vfs):
def put(self, source, dest):
aFile = xbmcvfs.File(xbmc.translatePath(source), 'r')
aFile = xbmcvfs.File(xbmcvfs.translatePath(source), 'r')
self.zip.writestr(dest, aFile.readBytes())
@@ -246,6 +257,16 @@ class DropboxFileSystem(Vfs):
else:
return False
def fileSize(self, filename):
result = 0
aFile = self._fix_slashes(filename)
if(self.client is not None):
metadata = self.client.files_get_metadata(aFile)
result = metadata.size / 1024 # bytes to KB
return result
def get_file(self, source, dest):
if(self.client is not None):
# write the file locally

View File

@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<settings>
<category id="general" label="30011">
<category id="general" label="30011" level="expert">
<setting id="compress_backups" type="bool" label="30087" default="false" />
<setting id="backup_rotation" type="number" label="30026" default="0" />
<setting id="always_prompt_restore_settings" type="bool" label="30148" default="false" />
<setting id="progress_mode" type="enum" label="30022" lvalues="30082|30083|30084" default="0" />
<setting type="sep" />
<setting id="verbose_logging" type="bool" label="Enable Verbose Logging" default="false" />
<setting id="upgrade_notes" type="number" label="upgrade_notes" visible="false" default="1" />
</category>
<category id="backup_path" label="30048">

4
service.py Normal file
View File

@@ -0,0 +1,4 @@
from resources.lib.scheduler import BackupScheduler
# start the backup scheduler
BackupScheduler().start()