Compare commits
218 Commits
krypton-1.
...
matrix-1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8d334352 | ||
|
|
c17a185639 | ||
|
|
aff124af1f | ||
|
|
350d81caf4 | ||
|
|
46d7d22523 | ||
|
|
11c644cb15 | ||
|
|
941b593751 | ||
|
|
e622a0455f | ||
|
|
9c1ecc254f | ||
|
|
44fdf7a20a | ||
|
|
ec214c074f | ||
|
|
02d852a7e9 | ||
|
|
d7e4946d9a | ||
|
|
6fdaa4f253 | ||
|
|
5f1f9fef38 | ||
|
|
4098cd18cb | ||
|
|
8119a09449 | ||
|
|
a0ccd85d9e | ||
|
|
71c8d9ae54 | ||
|
|
b470412b4f | ||
|
|
7a5886cd26 | ||
|
|
f20887b6e7 | ||
|
|
a0eb28a5f6 | ||
|
|
a198c9448a | ||
|
|
fa3a30eb55 | ||
|
|
201d04afeb | ||
|
|
2dabb23c2d | ||
|
|
2f19ec2b75 | ||
|
|
db215873cf | ||
|
|
bd963719d4 | ||
|
|
18b7f338c7 | ||
|
|
92a9245bdc | ||
|
|
568c3758a4 | ||
|
|
af999f7d04 | ||
|
|
1264ab86b2 | ||
|
|
90c458d4fc | ||
|
|
0e6f5acfb5 | ||
|
|
c9dd381037 | ||
|
|
55b2ac83d4 | ||
|
|
0d14dd17c6 | ||
|
|
9fa354b467 | ||
|
|
006485b19e | ||
|
|
3c2f512ecf | ||
|
|
190b4fd86f | ||
|
|
9ecf706d63 | ||
|
|
05c53b7ed8 | ||
|
|
8bc73f2832 | ||
|
|
edc4a7b20f | ||
|
|
90b4aeeebe | ||
|
|
7ce9123e1f | ||
|
|
8bfef6692f | ||
|
|
e63560f0c4 | ||
|
|
51f2ef3973 | ||
|
|
04bac77690 | ||
|
|
b1f6d36d73 | ||
|
|
5d398836ba | ||
|
|
23a14d67c4 | ||
|
|
0eebe1c5cc | ||
|
|
4d55385179 | ||
|
|
ac68001aa1 | ||
|
|
233dff0e15 | ||
|
|
6f69f80742 | ||
|
|
47fcb119f3 | ||
|
|
f9f49e3fe6 | ||
|
|
7c23c17e33 | ||
|
|
710bcd08f8 | ||
|
|
5e2d099448 | ||
|
|
8d66fa6a9f | ||
|
|
5ee610a586 | ||
|
|
8c4465f552 | ||
|
|
3849a902ea | ||
|
|
4492ab593e | ||
|
|
16e13c7d80 | ||
|
|
0cc0684263 | ||
|
|
dd5b99c978 | ||
|
|
6c99667afa | ||
|
|
6514b3db02 | ||
|
|
88341d9e1f | ||
|
|
95649c2b3f | ||
|
|
3e9de429dd | ||
|
|
db18c6a7b4 | ||
|
|
35e05acaf2 | ||
|
|
92ec8bf25c | ||
|
|
0c79aef4e7 | ||
|
|
fea7dca500 | ||
|
|
f7665c8ddd | ||
|
|
bbbfc3dd84 | ||
|
|
0b03914175 | ||
|
|
51553f7720 | ||
|
|
294683fb43 | ||
|
|
b74c1af704 | ||
|
|
edd4002d3f | ||
|
|
3aa912ca4c | ||
|
|
5c3e1712f6 | ||
|
|
82bdc955b5 | ||
|
|
4f1e5060e9 | ||
|
|
7d895a6028 | ||
|
|
7ede17fbbd | ||
|
|
d32620ea18 | ||
|
|
def99767e8 | ||
|
|
c7a9a8512d | ||
|
|
332afffc5b | ||
|
|
42d0f1b451 | ||
|
|
8d07310980 | ||
|
|
048d016e0e | ||
|
|
c50c5245fc | ||
|
|
e91037208b | ||
|
|
ff2ca53a22 | ||
|
|
1a27b279b0 | ||
|
|
6dfa4a5520 | ||
|
|
a7b9aeb9c1 | ||
|
|
7226178bfb | ||
|
|
f5bd7130e2 | ||
|
|
ae76d24e86 | ||
|
|
4d56331d8f | ||
|
|
45cf9a367d | ||
|
|
d8ceecb168 | ||
|
|
a2d7e8613a | ||
|
|
c0b0fa82cb | ||
|
|
6ac1d3559b | ||
|
|
d93589ecad | ||
|
|
b21c11de26 | ||
|
|
a4bb3f3feb | ||
|
|
1f6324b2d5 | ||
|
|
12b25f7cea | ||
|
|
5d9d8a1820 | ||
|
|
2fdf8d37fe | ||
|
|
aa94060cfe | ||
|
|
b9e0424ea5 | ||
|
|
495ecb1048 | ||
|
|
a1c0c0bbfe | ||
|
|
9f570233d9 | ||
|
|
b38aff2a8e | ||
|
|
456ebe9374 | ||
|
|
30f8b93629 | ||
|
|
94f872fb81 | ||
|
|
8f8402ae8a | ||
|
|
db93e40f59 | ||
|
|
72c77fb33a | ||
|
|
1f0e262c5b | ||
|
|
b75487bb2a | ||
|
|
b34e538d6b | ||
|
|
b5a7aada4c | ||
|
|
1a9c43b998 | ||
|
|
b7f4b14fe2 | ||
|
|
787b054bba | ||
|
|
a7be48a341 | ||
|
|
2fe76b7b52 | ||
|
|
3aed105fd7 | ||
|
|
c9b4554eac | ||
|
|
e736b964a5 | ||
|
|
4c5f6774df | ||
|
|
1f2e315208 | ||
|
|
68093b2130 | ||
|
|
701a1831bf | ||
|
|
493e0d3a2e | ||
|
|
d87e209226 | ||
|
|
9960e2fc6b | ||
|
|
6aae9d9247 | ||
|
|
004b8dae58 | ||
|
|
6b934ed30c | ||
|
|
e950400222 | ||
|
|
cb2bb8a237 | ||
|
|
eb765c974b | ||
|
|
d18ed2960e | ||
|
|
9f1755686c | ||
|
|
534b3b108f | ||
|
|
4a8b891129 | ||
|
|
49af21a67e | ||
|
|
3ee2cb0414 | ||
|
|
061fd3efed | ||
|
|
76c2fdc0c2 | ||
|
|
2c999b46b9 | ||
|
|
4d891ab551 | ||
|
|
6c33e7c9ba | ||
|
|
f0d8e297a9 | ||
|
|
04ec3bd8a8 | ||
|
|
65ea3c98c4 | ||
|
|
4108f333e2 | ||
|
|
913090637c | ||
|
|
7139b920ad | ||
|
|
48d07c24a0 | ||
|
|
90e4b0c1f4 | ||
|
|
c9415cbf59 | ||
|
|
181654b414 | ||
|
|
71b048418e | ||
|
|
1896a684b0 | ||
|
|
545bd93e8c | ||
|
|
83a01a48bf | ||
|
|
dcc8482d73 | ||
|
|
85306f9469 | ||
|
|
c0d3b01ade | ||
|
|
5c6a8ce91a | ||
|
|
c8f148cd1a | ||
|
|
b6e57b04b1 | ||
|
|
adbf225ea2 | ||
|
|
d9d6c1ed42 | ||
|
|
f5f7bcfcb5 | ||
|
|
4608f04d96 | ||
|
|
e1c8b5a61c | ||
|
|
ada1efb165 | ||
|
|
5a43b5e340 | ||
|
|
33bc84c288 | ||
|
|
8c61616d3c | ||
|
|
622939901e | ||
|
|
756f50bba1 | ||
|
|
5fefbd286d | ||
|
|
5779784e0a | ||
|
|
24f570e888 | ||
|
|
489dcd317f | ||
|
|
216e2f4561 | ||
|
|
c3fe86293d | ||
|
|
20ee7a92ad | ||
|
|
64daaa13e8 | ||
|
|
dda08d04a3 | ||
|
|
469b5ff340 | ||
|
|
ff2f764b2f | ||
|
|
7d51ee05bc |
7
.gitattributes
vendored
Normal 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
|
||||
18
.github/stale-dontuse.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 31
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed
|
||||
daysUntilClose: 14
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels:
|
||||
- waiting for info
|
||||
- wontfix
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: inactive
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as inactive because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
2
.settings/org.eclipse.core.resources.prefs
Normal file
@@ -0,0 +1,2 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding//resources/lib/croniter.py=utf-8
|
||||
20
.travis.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
dist: xenial
|
||||
language: python
|
||||
python: 3.7
|
||||
|
||||
install:
|
||||
- pip install flake8 flake8_polyfill kodi-addon-checker git+https://github.com/romanvm/kodi-addon-submitter.git
|
||||
|
||||
before_script:
|
||||
- git config core.quotepath false
|
||||
|
||||
# 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
|
||||
|
||||
deploy:
|
||||
- provider: script
|
||||
script: submit-addon -r repo-scripts -b matrix --push-branch script.xbmcbackup
|
||||
on:
|
||||
tags: true
|
||||
@@ -1,6 +1,5 @@
|
||||
# Backup Addon
|
||||
|
||||
__Kodi Version Compatibility:__ Kodi 17.x (Krypton) and greater
|
||||
  [](https://travis-ci.com/robweber/xbmcbackup) [](https://github.com/robweber/xbmcbackup/blob/master/LICENSE.txt) [](https://www.python.org/dev/peps/pep-0008/)
|
||||
|
||||
## About
|
||||
|
||||
|
||||
44
addon.xml
@@ -1,20 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="script.xbmcbackup"
|
||||
name="Backup" version="1.5.0" provider-name="robweber">
|
||||
name="Backup" version="1.6.5" provider-name="robweber">
|
||||
<requires>
|
||||
<!-- jarvis -->
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.httplib2" version="0.8.0" />
|
||||
<import addon="script.module.oauth2client" version="4.1.2" />
|
||||
<import addon="script.module.uritemplate" version="0.6" />
|
||||
<import addon="script.module.yaml" version="3.11"/>
|
||||
<import addon="script.module.googleapi" version="1.6.4" />
|
||||
<import addon="script.module.requests" version="2.9.1" />
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.dateutil" version="2.8.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" start="startup" />
|
||||
<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>
|
||||
@@ -82,23 +78,21 @@
|
||||
<description lang="sk_SK">Už ste niekedy poškodili konfiguráciu Kodi a priali si mať zálohu? Teraz môžete - na jeden klik. Môžete exportovať Vašu databázu, playlist, náhľady, doplnky a konfigurácie na ktorýkoľvek zdroj zapisovateľný Kodi. Zálohy môžu byť púšťané na požiadanie alebo plánovačom. </description>
|
||||
<description lang="sv_SE">Har du någonsin tappat bort din Kodi konfiguration och önskat att du hade en backup? Nu kan du enkelt med ett klick. Du kan exportera din databas, spellista, minityrer, tillägg och andra konfigurationsdetaljer till valfri källa som är skrivbar för Kodi. Backupper kan köras på begäran eller via scheman.</description>
|
||||
<description lang="zh_CN">你是否经常折腾你的 Kodi,因而希望能够有个备份?你可以把资料库、播放列表、缩略图、插件和其他配置细节导出到 Kodi 可以写入的任意位置。备份可以按需运行或通过计划任务执行。</description>
|
||||
<language></language>
|
||||
<platform>all</platform>
|
||||
<license>The MIT License</license>
|
||||
<license>MIT</license>
|
||||
<forum>https://forum.kodi.tv/showthread.php?tid=129499</forum>
|
||||
<source>https://github.com/robweber/xbmcbackup</source>
|
||||
<email></email>
|
||||
<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.1.4
|
||||
- added file chunk support for dropbox uploads
|
||||
- fixed settings duplicate ids, thanks aster-anto
|
||||
- added scheduler delay to assist with time sync (rpi mostly)
|
||||
</news>
|
||||
<news>Version 1.6.5
|
||||
- updated to new settings format with levels
|
||||
- added ability to change path where temp zip file is built
|
||||
- fixed issues with xbmcgui Dialogs
|
||||
</news>
|
||||
</extension>
|
||||
</addon>
|
||||
|
||||
466
changelog.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# Changelog
|
||||
|
||||
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.5](https://github.com/robweber/xbmcbackup/compare/matrix-1.6.4...robweber:matrix-1.6.5) - 2021-03-06
|
||||
|
||||
### Added
|
||||
|
||||
- added Expert setting to change location of zip file temp location as it's being built or extracted
|
||||
|
||||
### Changed
|
||||
|
||||
- updated ```settings.xml``` to match new [Kodi settings syntax](https://kodi.wiki/view/Add-on_settings_conversion), including visibility levels
|
||||
|
||||
### Fixed
|
||||
|
||||
- when restoring from a zip file the command to delete the extracted directory was incorrect
|
||||
- ```Dialog().yesno()``` no longer takes line1 arg, changed to message
|
||||
|
||||
## [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
|
||||
|
||||
- added new badges for Kodi Version, TravisCI and license information from shields.io
|
||||
- dependency on script.module.dateutil for relativedelta.py class
|
||||
|
||||
### Changed
|
||||
|
||||
- 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
|
||||
- updated code to make python3 compatible
|
||||
- updated code for pep9 styling
|
||||
- use setArt() to set ListItem icons as the icon= constructor is deprecated
|
||||
- Dropbox dependency is now 9.4.0
|
||||
|
||||
### Removed
|
||||
|
||||
- removed need for urlparse library
|
||||
- Removed GoogleDrive support - issues with python 3 compatibility
|
||||
- removed relativedelta.py, use the dateutil module for this
|
||||
|
||||
## [Version 1.5.2](https://github.com/robweber/xbmcbackup/compare/krypton-1.5.1...robweber:krypton-1.5.2) - 2019-09-30
|
||||
|
||||
### Added
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
### Removed
|
||||
|
||||
- files releated to dropbox library, using script.module.dropbox import now
|
||||
|
||||
## Version 1.5.1 - 2019-09-10
|
||||
|
||||
### Changed
|
||||
- Fixed guisettings restores not working - thanks Bluerayx
|
||||
|
||||
## Version 1.5.0 - 2019-08-26
|
||||
|
||||
### Added
|
||||
- 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
|
||||
|
||||
## Version 1.1.3 - 2017-12-29
|
||||
|
||||
### 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
|
||||
|
||||
## Version 1.1.2
|
||||
|
||||
### Added
|
||||
- Fixes to the Dropbox lib for python 2.6
|
||||
|
||||
## Version 1.1.1
|
||||
|
||||
### Added
|
||||
- added ability to "catchup" on missed scheduled backup
|
||||
|
||||
### Changed
|
||||
- fixed error on authorizers (missing secret/key)
|
||||
- updated google oauth and client versions
|
||||
- merged in dropbox v2 library code
|
||||
|
||||
## Version 1.1.0
|
||||
|
||||
### Added
|
||||
- added tinyurl generation for oauth urls
|
||||
|
||||
### Changed
|
||||
- moved authorize to settings area for cloud storage
|
||||
|
||||
## Version 1.0.9
|
||||
|
||||
### Changed
|
||||
- fixed dropbox rest.py for Python 2.6 - thanks koying!
|
||||
|
||||
## Version 1.0.8
|
||||
|
||||
### Changed
|
||||
- updated dropbox api
|
||||
|
||||
## Version 1.0.7
|
||||
|
||||
### Changed
|
||||
- updated google client api version
|
||||
|
||||
## Version 1.0.6
|
||||
|
||||
### Added
|
||||
|
||||
- added progress for zip extraction - hopefully helps with extract errors
|
||||
|
||||
### Changed
|
||||
- fix for custom directories not working recursively
|
||||
|
||||
## Version 1.0.5
|
||||
|
||||
### Added
|
||||
- added google drive support
|
||||
- added settings dialog option - thanks ed_davidson
|
||||
|
||||
### Changed
|
||||
- make compression setting compatible with python 2.6 and above
|
||||
- fix for growing backups - thanks brokeh
|
||||
|
||||
## Version 1.0.4
|
||||
|
||||
### Added
|
||||
- exit if we can't delete the old archive, non recoverable
|
||||
|
||||
## Version 1.0.3
|
||||
|
||||
### Added
|
||||
- added "delete auth" dialog to delete oauth files in settings
|
||||
|
||||
## Version 1.0.2
|
||||
|
||||
### Changed
|
||||
- updated xbmc.python version to 2.19.0 - should be helix only
|
||||
|
||||
## Version 1.0.0
|
||||
|
||||
### Changed
|
||||
- rebranded as "Backup"
|
||||
- removed XBMC references and replaced with Kodi
|
||||
- tweaked file walking for Helix
|
||||
|
||||
## Version 0.5.9
|
||||
|
||||
### Added
|
||||
|
||||
- create restored version of guisettings for easy local restoration
|
||||
|
||||
### Changed
|
||||
- fixed dropbox unicode error
|
||||
|
||||
## Version 0.5.8.7
|
||||
|
||||
### Added
|
||||
- allow limited updating of guisettings file through json
|
||||
|
||||
## Version 0.5.8.6
|
||||
|
||||
### Added
|
||||
- show notification if some files failed
|
||||
- check if destination is writeable - thanks war59312
|
||||
|
||||
## Version 0.5.8.5
|
||||
|
||||
### Added
|
||||
- added custom library nodes to config backup options - thanks Ned Scott
|
||||
|
||||
## Version 0.5.8.4
|
||||
|
||||
### Changed
|
||||
- backup compression should use zip64 as sizes may be over 2GB
|
||||
- need to expand out path -bugfix
|
||||
|
||||
## Version 0.5.8
|
||||
|
||||
- fixes path substitution errors
|
||||
|
||||
## Version 0.5.7
|
||||
|
||||
- added option to compress backups, uses local source for staging the
|
||||
zip before sending to remote
|
||||
|
||||
## Version 0.5.6
|
||||
|
||||
- fix dropbox delete recursion error - thanks durd updated language
|
||||
files
|
||||
|
||||
## Version 0.5.5
|
||||
|
||||
- fix for dropbox errors during repeated file upload attempts
|
||||
|
||||
## Version 0.5.4
|
||||
|
||||
- check xbmc version when doing a restore
|
||||
|
||||
## Version 0.5.3
|
||||
|
||||
- updated python version
|
||||
|
||||
## Version 0.5.2
|
||||
|
||||
- added additional script and window parameters, thanks Samu-rai
|
||||
- critical error in backup rotation
|
||||
- updated progress bar display
|
||||
|
||||
## Version 0.5.1
|
||||
|
||||
- updated for new Gotham xbmc python updates
|
||||
|
||||
## Version 0.5.0
|
||||
|
||||
- New Version for Gotham
|
||||
|
||||
## Version 0.4.6
|
||||
|
||||
- modified backup folder names to include time, also modified display
|
||||
listing
|
||||
|
||||
## Version 0.4.5
|
||||
|
||||
- added version info to logs
|
||||
- added try/catch for unicode errors
|
||||
|
||||
## Version 0.4.4
|
||||
|
||||
- modified the check for invalid file types
|
||||
|
||||
## Version 0.4.3
|
||||
|
||||
- added error message if remote directory is blank
|
||||
- added license tag
|
||||
|
||||
## Version 0.4.2
|
||||
|
||||
- Added support for userdata/profiles folder - thanks TUSSFC
|
||||
|
||||
## Version 0.4.1
|
||||
|
||||
- added encode() around notifications
|
||||
|
||||
## Version 0.4.0
|
||||
|
||||
- fixed settings display error - thanks zer04c
|
||||
|
||||
## Version 0.3.9
|
||||
|
||||
- added "just once" scheduler for one-off type backups
|
||||
- show notification on scheduler
|
||||
- update updated language files from Transifex
|
||||
|
||||
## Version 0.3.8
|
||||
|
||||
- added advancedsettings check on restore. prompts user to restore only this file and restart xbmc to continue. This fixes issues where path substitution was not working during restores - thanks ctrlbru
|
||||
|
||||
## [Version 0.3.7]
|
||||
|
||||
- added optional addon.xml tags
|
||||
- update language files from Transifex
|
||||
|
||||
## Version 0.3.6
|
||||
|
||||
- added up to 2 custom directories, can be toggled on/off
|
||||
- added a check for backup verification before rotation - no more
|
||||
deleting non backup related files
|
||||
- use monitor class for onSettingsChanged method
|
||||
|
||||
## Version 0.3.5
|
||||
|
||||
- test of custom directories - only 1 at the moment
|
||||
|
||||
## Version 0.3.4
|
||||
|
||||
- added ability to take parameters via RunScript() or
|
||||
JSONRPC.Addons.ExecuteAddon()
|
||||
|
||||
## Version 0.3.3
|
||||
|
||||
- updated xbmc python version (2.1.0)
|
||||
|
||||
## Version 0.3.2
|
||||
|
||||
- added settings for user provided Dropbox key and secret
|
||||
|
||||
## Version 0.3.1
|
||||
|
||||
- added try/except for multiple character encodings
|
||||
- remove token.txt file if Dropbox Authorization is revoked
|
||||
- can shutdown xbmc after scheduled backup
|
||||
|
||||
## Version 0.3.0
|
||||
|
||||
- major vfs rewrite
|
||||
- Added Dropbox as storage target
|
||||
- updated gui/removed settings - thanks SFX Group for idea!
|
||||
|
||||
## Version 0.2.3
|
||||
|
||||
- first official frodo build
|
||||
|
||||
## Version 0.2.2
|
||||
|
||||
- fix for backup rotation sort
|
||||
|
||||
## Version 0.2.1
|
||||
|
||||
- added ability to rotate backups, keeping a set number of days
|
||||
|
||||
## Version 0.2.0
|
||||
|
||||
- removed the vfs.py helper library
|
||||
- default.py file now uses xbmcvfs python library exclusively for
|
||||
listing directories and copy operations
|
||||
|
||||
## Version 0.1.7
|
||||
|
||||
- minor bug fixes and translations updates
|
||||
|
||||
## Version 0.1.6
|
||||
|
||||
- merged scheduler branch with master, can now schedule backups on an
|
||||
interval
|
||||
|
||||
## Version 0.1.5
|
||||
|
||||
- pulled xbmcbackup class into separate library
|
||||
|
||||
## Version 0.1.4
|
||||
|
||||
- added more verbose error message for incorrect paths
|
||||
|
||||
## Version 0.1.3
|
||||
|
||||
- backup folder format - thanks zeroram
|
||||
- added German translations - thanks dersphere
|
||||
- removed need for separate verbose logging setting
|
||||
- updated utf-8 encoding for all logging
|
||||
- backup now uses date as folder name, restore allows user to type date
|
||||
of last backup
|
||||
|
||||
## Version 0.1.2
|
||||
|
||||
- added French language translation - thanks mikebzh44
|
||||
- added some utf-8 encoding tags to filenames
|
||||
|
||||
## Version 0.1.1
|
||||
|
||||
- added check for key in vfs.py - Thanks Martijn!
|
||||
|
||||
## Version 0.1.0
|
||||
|
||||
- removed transparency from icon.png
|
||||
|
||||
## Version 0.0.9
|
||||
|
||||
- modified vfs.py again to filter out xsp files (smart playlists).
|
||||
Created running list for these types of compressed files
|
||||
- added enable/disable logging toggle in settings
|
||||
|
||||
## Version 0.0.8
|
||||
|
||||
- modified vfs.py script to exclude handling zip files as directories,
|
||||
added keymap and peripheral data folders in the "config" section
|
||||
|
||||
## Version 0.0.7
|
||||
|
||||
- removed "restore.txt" file and now write file listing to memory list
|
||||
instead
|
||||
|
||||
## Version 0.0.6
|
||||
|
||||
- Added the vfs module created by paddycarey
|
||||
- File Selection is now followed for both backup and restore options
|
||||
|
||||
## Version 0.0.5
|
||||
|
||||
- Added option to manually type a path rather than browse for one (only
|
||||
one used)
|
||||
- Show progress bar right away so you know this is doing something
|
||||
|
||||
## Version 0.0.4
|
||||
|
||||
- Finished code for restore mode.
|
||||
|
||||
## Version 0.0.3
|
||||
|
||||
- Added progress bar and "silent" option for running on startup or as a
|
||||
script
|
||||
|
||||
## Version 0.0.2
|
||||
|
||||
- First version, should backup directories as needed
|
||||
318
changelog.txt
@@ -1,318 +0,0 @@
|
||||
Version 1.5.0
|
||||
|
||||
Overhaul of file selection and restore procedures. Breaking Change with previous versions PR117
|
||||
|
||||
Version 1.1.3
|
||||
|
||||
added file chunk support for dropbox uploads
|
||||
fixed settings duplicate ids, thanks aster-anto
|
||||
added scheduler delay to assist with time sync (rpi mostly)
|
||||
|
||||
Version 1.1.2
|
||||
|
||||
added fixes to the Dropbox lib for python 2.6
|
||||
|
||||
Version 1.1.1
|
||||
|
||||
fixed error on authorizers (missing secret/key)
|
||||
added ability to "catchup" on missed scheduled backup
|
||||
updated google oauth and client versions
|
||||
merged in dropbox v2 code
|
||||
|
||||
Version 1.1.0
|
||||
|
||||
added tinyurl for oauth urls
|
||||
moved authorize to settings area for cloud storage
|
||||
bug fixes
|
||||
|
||||
Version 1.0.9
|
||||
|
||||
fixed dropbox rest.py for Python 2.6 - thanks koying!
|
||||
|
||||
Version 1.0.8
|
||||
|
||||
bug fixes
|
||||
updated dropbox api
|
||||
|
||||
Version 1.0.7
|
||||
|
||||
updated google client api version
|
||||
|
||||
Version 1.0.6
|
||||
|
||||
fix for custom directories not working recursively
|
||||
added progress for zip extraction - hopefully helps with extract errors
|
||||
|
||||
Version 1.0.5
|
||||
|
||||
added google drive support
|
||||
make compression setting compatible with python 2.6 and above
|
||||
added settings dialog option - thanks ed_davidson
|
||||
fix for growing backups - thanks brokeh
|
||||
|
||||
Version 1.0.4
|
||||
|
||||
exit if we can't delete the old archive, non recoverable
|
||||
|
||||
Version 1.0.3
|
||||
|
||||
added "delete auth" dialog to delete oauth files in settings
|
||||
|
||||
Version 1.0.2
|
||||
|
||||
updated xbmc.python version to 2.19.0 - should be helix only
|
||||
|
||||
Version 1.0.0
|
||||
|
||||
rebranded as "Backup"
|
||||
removed XBMC references and replaced with Kodi
|
||||
tweaked file walking for Helix
|
||||
|
||||
Version 0.5.9
|
||||
|
||||
fixed dropbox unicode error
|
||||
create restored version of guisettings for easy local restoration
|
||||
|
||||
Version 0.5.8.7
|
||||
|
||||
allow limited updating of guisettings file through json
|
||||
|
||||
Version 0.5.8.6
|
||||
|
||||
show notification if some files failed
|
||||
check if destination is writeable - thanks war59312
|
||||
|
||||
Version 0.5.8.5
|
||||
|
||||
added custom library nodes to config backup options - thanks Ned Scott
|
||||
|
||||
Version 0.5.8.4
|
||||
|
||||
backup compression should use zip64 as sizes may be over 2GB
|
||||
need to expand out path -bugfix
|
||||
|
||||
Version 0.5.8
|
||||
|
||||
fixes path substitution errors
|
||||
|
||||
Version 0.5.7
|
||||
|
||||
added option to compress backups, uses local source for staging the zip before sending to remote
|
||||
|
||||
Version 0.5.6
|
||||
|
||||
fix dropbox delete recursion error - thanks durd
|
||||
updated language files
|
||||
|
||||
Version 0.5.5
|
||||
|
||||
fix for dropbox errors during repeated file upload attempts
|
||||
|
||||
Version 0.5.4
|
||||
|
||||
check xbmc version when doing a restore
|
||||
|
||||
Version 0.5.3
|
||||
|
||||
updated python version
|
||||
|
||||
Version 0.5.2
|
||||
|
||||
added additional script and window parameters, thanks Samu-rai
|
||||
|
||||
critical error in backup rotation
|
||||
|
||||
updated progress bar display
|
||||
|
||||
Version 0.5.1
|
||||
|
||||
updated for new Gotham xbmc python updates
|
||||
|
||||
Version 0.5.0
|
||||
|
||||
New Version for Gotham
|
||||
|
||||
Version 0.4.6
|
||||
|
||||
modified backup folder names to include time, also modified display listing
|
||||
|
||||
Version 0.4.5
|
||||
|
||||
added version info to logs
|
||||
|
||||
added try/catch for unicode errors
|
||||
|
||||
Version 0.4.4
|
||||
|
||||
modified the check for invalid file types
|
||||
|
||||
Version 0.4.3
|
||||
|
||||
added error message if remote directory is blank
|
||||
|
||||
added license tag
|
||||
|
||||
Version 0.4.2
|
||||
|
||||
Added support for userdata/profiles folder - thanks TUSSFC
|
||||
|
||||
Version 0.4.1
|
||||
|
||||
added encode() around notifications
|
||||
|
||||
Version 0.4.0
|
||||
|
||||
fixed settings display error - thanks zer04c
|
||||
|
||||
Version 0.3.9
|
||||
|
||||
added "just once" scheduler for one-off type backups
|
||||
show notification on scheduler update
|
||||
updated language files from Transifex
|
||||
|
||||
Version 0.3.8
|
||||
|
||||
added advancedsettings check on restore. prompts user to restore only this file and restart xbmc to continue. This fixes issues where path substitution was not working during restores - thanks ctrlbru
|
||||
|
||||
Version 0.3.7
|
||||
|
||||
added optional addon.xml tags
|
||||
|
||||
update language files from Transifex
|
||||
|
||||
Version 0.3.6
|
||||
|
||||
added up to 2 custom directories, can be toggled on/off
|
||||
|
||||
added a check for backup verification before rotation - no more deleting non backup related files
|
||||
|
||||
use monitor class for onSettingsChanged method
|
||||
|
||||
Version 0.3.5
|
||||
|
||||
test of custom directories - only 1 at the moment
|
||||
|
||||
Version 0.3.4
|
||||
|
||||
added ability to take parameters via RunScript() or JSONRPC.Addons.ExecuteAddon()
|
||||
|
||||
Version 0.3.3
|
||||
|
||||
updated xbmc python version (2.1.0)
|
||||
|
||||
Version 0.3.2
|
||||
|
||||
added settings for user provided Dropbox key and secret
|
||||
|
||||
Version 0.3.1
|
||||
|
||||
added try/except for multiple character encodings
|
||||
|
||||
remove token.txt file if Dropbox Authorization is revoked
|
||||
|
||||
can shutdown xbmc after scheduled backup
|
||||
|
||||
Version 0.3.0
|
||||
|
||||
major vfs rewrite
|
||||
|
||||
Added Dropbox as storage target
|
||||
|
||||
updated gui/removed settings - thanks SFX Group for idea!
|
||||
|
||||
Version 0.2.3
|
||||
|
||||
first official frodo build
|
||||
|
||||
Version 0.2.2
|
||||
|
||||
fix for backup rotation sort
|
||||
|
||||
Version 0.2.1
|
||||
|
||||
added ability to rotate backups, keeping a set number of days
|
||||
|
||||
Version 0.2.0
|
||||
|
||||
removed the vfs.py helper library
|
||||
|
||||
default.py file now uses xbmcvfs python library exclusively for listing directories and copy operations
|
||||
|
||||
Version 0.1.7
|
||||
|
||||
minor bug fixes and translations updates
|
||||
|
||||
Version 0.1.6
|
||||
|
||||
merged scheduler branch with master, can now schedule backups on an interval
|
||||
|
||||
Version 0.1.5
|
||||
|
||||
pulled xbmcbackup class into separate library
|
||||
|
||||
Version 0.1.4
|
||||
|
||||
added more verbose error message for incorrect paths
|
||||
|
||||
Version 0.1.3
|
||||
|
||||
backup folder format - thanks zeroram
|
||||
|
||||
added German translations - thanks dersphere
|
||||
|
||||
removed need for separate verbose logging setting
|
||||
|
||||
updated utf-8 encoding for all logging
|
||||
|
||||
backup now uses date as folder name, restore allows user to type date of last backup
|
||||
|
||||
Version 0.1.2
|
||||
|
||||
added French language translation - thanks mikebzh44
|
||||
|
||||
added some utf-8 encoding tags to filenames
|
||||
|
||||
Version 0.1.1
|
||||
|
||||
added check for key in vfs.py - Thanks Martijn!
|
||||
|
||||
Version 0.1.0
|
||||
|
||||
removed transparency from icon.png
|
||||
|
||||
Version 0.0.9
|
||||
|
||||
modified vfs.py again to filter out xsp files (smart playlists). Created running list for these types of compressed files
|
||||
|
||||
added enable/disable logging toggle in settings
|
||||
|
||||
Version 0.0.8
|
||||
|
||||
modified vfs.py script to exclude handling zip files as directories, added keymap and peripheral data folders in the "config" section
|
||||
|
||||
Version 0.0.7
|
||||
|
||||
removed "restore.txt" file and now write file listing to memory list instead
|
||||
|
||||
Version 0.0.6
|
||||
|
||||
Added the vfs module created by paddycarey
|
||||
File Selection is now followed for both backup and restore options
|
||||
|
||||
Version 0.0.5
|
||||
|
||||
Added option to manually type a path rather than browse for one (only one used)
|
||||
Show progress bar right away so you know this is doing something
|
||||
|
||||
Version 0.0.4
|
||||
|
||||
Finished code for restore mode.
|
||||
|
||||
Version 0.0.3
|
||||
|
||||
Added progress bar and "silent" option for running on startup or as a script
|
||||
|
||||
Version 0.0.2
|
||||
|
||||
First version, should backup directories as needed
|
||||
|
||||
70
default.py
@@ -1,21 +1,26 @@
|
||||
import sys, urlparse
|
||||
import xbmc, xbmcgui
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import resources.lib.utils as utils
|
||||
from resources.lib.backup import XbmcBackup
|
||||
|
||||
|
||||
def get_params():
|
||||
param = {}
|
||||
|
||||
if(len(sys.argv) > 1):
|
||||
try:
|
||||
for i in sys.argv:
|
||||
args = i
|
||||
if(args.startswith('?')):
|
||||
args = args[1:]
|
||||
param.update(dict(urlparse.parse_qsl(args)))
|
||||
|
||||
if('=' in args):
|
||||
if(args.startswith('?')):
|
||||
args = args[1:] # legacy in case of url params
|
||||
splitString = args.split('=')
|
||||
param[splitString[0]] = splitString[1]
|
||||
except:
|
||||
pass
|
||||
|
||||
return param
|
||||
|
||||
#the program mode
|
||||
|
||||
# the program mode
|
||||
mode = -1
|
||||
params = get_params()
|
||||
|
||||
@@ -26,37 +31,38 @@ if("mode" in params):
|
||||
elif(params['mode'] == 'restore'):
|
||||
mode = 1
|
||||
|
||||
#if mode wasn't passed in as arg, get from user
|
||||
|
||||
# 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):
|
||||
# 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(utils.getSettingInt('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)
|
||||
# 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
|
||||
# check if program should be run
|
||||
if(mode != -1):
|
||||
#run the profile backup
|
||||
# run the profile backup
|
||||
backup = XbmcBackup()
|
||||
|
||||
if(mode == 2):
|
||||
#open the settings dialog
|
||||
# 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(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()):
|
||||
|
||||
if(mode == backup.Restore):
|
||||
#get list of valid restore points
|
||||
# get list of valid restore points
|
||||
restorePoints = backup.listBackups()
|
||||
pointNames = []
|
||||
folderNames = []
|
||||
|
||||
|
||||
for aDir in restorePoints:
|
||||
pointNames.append(aDir[1])
|
||||
folderNames.append(aDir[0])
|
||||
@@ -64,21 +70,21 @@ if(mode != -1):
|
||||
selectedRestore = -1
|
||||
|
||||
if("archive" in params):
|
||||
#check that the user give archive exists
|
||||
# check that the user give archive exists
|
||||
if(params['archive'] in folderNames):
|
||||
#set the index
|
||||
# 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)
|
||||
# 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:
|
||||
@@ -86,6 +92,6 @@ if(mode != -1):
|
||||
else:
|
||||
backup.backup()
|
||||
else:
|
||||
#can't go any further
|
||||
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30045))
|
||||
# can't go any further
|
||||
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30045))
|
||||
utils.openSettings()
|
||||
|
||||
44
launcher.py
@@ -1,55 +1,49 @@
|
||||
# launcher for various helpful functions found in the settings.xml area
|
||||
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.authorizers import DropboxAuthorizer
|
||||
from resources.lib.advanced_editor import AdvancedBackupEditor
|
||||
|
||||
|
||||
#launcher for various helpful functions found in the settings.xml area
|
||||
|
||||
def authorize_cloud(cloudProvider):
|
||||
#drobpox
|
||||
# drobpox
|
||||
if(cloudProvider == 'dropbox'):
|
||||
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)))
|
||||
|
||||
#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)
|
||||
# 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
|
||||
|
||||
# delete any of the known token file types
|
||||
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():
|
||||
param = {}
|
||||
try:
|
||||
for i in sys.argv:
|
||||
args = i
|
||||
if(args.startswith('?')):
|
||||
args = args[1:]
|
||||
param.update(dict(urlparse.parse_qsl(args)))
|
||||
if('=' in args):
|
||||
if(args.startswith('?')):
|
||||
args = args[1:] # legacy in case of url params
|
||||
splitString = args.split('=')
|
||||
param[splitString[0]] = splitString[1]
|
||||
except:
|
||||
pass
|
||||
|
||||
return param
|
||||
|
||||
|
||||
params = get_params()
|
||||
|
||||
if(params['action'] == 'authorize_cloud'):
|
||||
|
||||
2
resources/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
|
||||
*.pyo
|
||||
|
Before Width: | Height: | Size: 125 KiB |
BIN
resources/images/screenshot1.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 129 KiB |
BIN
resources/images/screenshot2.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 270 KiB |
BIN
resources/images/screenshot3.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 188 KiB |
BIN
resources/images/screenshot4.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
resources/images/screenshot5.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
resources/images/screenshot6.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
@@ -173,8 +173,8 @@ msgid "Would you like to continue?"
|
||||
msgstr "Would you like to continue?"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Error: Remote path doesn't exist"
|
||||
msgstr "Error: Remote path doesn't exist"
|
||||
msgid "Error: Remote or zip file path doesn't exist"
|
||||
msgstr "Error: Remote or zip file path doesn't exist"
|
||||
|
||||
msgctxt "#30046"
|
||||
msgid "Starting"
|
||||
@@ -555,4 +555,72 @@ 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 ""
|
||||
|
||||
msgctxt "#30151"
|
||||
msgid "Enable Verbose Logging"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30152"
|
||||
msgid "Set Zip File Location"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30153"
|
||||
msgid "Full path to where the zip file will be staged during backup or restore - must be local to this device"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30154"
|
||||
msgid "Always prompt if Kodi settings should be restored - no by default"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30155"
|
||||
msgid "Adds additional information to the log file"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30156"
|
||||
msgid "Must save key/secret first, then return to settings to authorize"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30157"
|
||||
msgid "Simple uses pre-defined folder locations, use Advanced Editor to define custom paths"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30158"
|
||||
msgid "Run backup on daily, weekly, monthly, or custom schedule"
|
||||
msgstr ""
|
||||
|
||||
2
resources/lib/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
|
||||
*.pyo
|
||||
@@ -1,229 +1,232 @@
|
||||
import json
|
||||
import utils as utils
|
||||
import xbmcvfs
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
import os.path
|
||||
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):
|
||||
self.paths = {}
|
||||
|
||||
#try and read in the custom file
|
||||
|
||||
# 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}]}
|
||||
def addSet(self, aSet):
|
||||
self.paths[aSet['name']] = {'root': aSet['root'], 'dirs': [{"type": "include", "path": aSet['root'], 'recurse': True}]}
|
||||
|
||||
#save the file
|
||||
# save the file
|
||||
self._writeFile()
|
||||
|
||||
def updateSet(self,name,aSet):
|
||||
def updateSet(self, name, aSet):
|
||||
self.paths[name] = aSet
|
||||
|
||||
#save the file
|
||||
# save the file
|
||||
self._writeFile()
|
||||
|
||||
def deleteSet(self,index):
|
||||
#match the index to a key
|
||||
def deleteSet(self, index):
|
||||
# match the index to a key
|
||||
keys = self.getSets()
|
||||
|
||||
#delete this set
|
||||
# delete this set
|
||||
del self.paths[keys[index]]
|
||||
|
||||
#save the file
|
||||
# save the file
|
||||
self._writeFile()
|
||||
|
||||
|
||||
def getSets(self):
|
||||
#list all current sets by name
|
||||
keys = self.paths.keys()
|
||||
# list all current sets by name
|
||||
keys = list(self.paths.keys())
|
||||
keys.sort()
|
||||
|
||||
return keys
|
||||
|
||||
def getSet(self,index):
|
||||
keys = self.getSets();
|
||||
def getSet(self, index):
|
||||
keys = self.getSets()
|
||||
|
||||
#return the set at this index
|
||||
return {'name':keys[index],'set':self.paths[keys[index]]}
|
||||
# return the set at this index
|
||||
return {'name': keys[index], 'set': self.paths[keys[index]]}
|
||||
|
||||
def validateSetName(self,name):
|
||||
def validateSetName(self, name):
|
||||
return (name not in self.getSets())
|
||||
|
||||
def _writeFile(self):
|
||||
#create the custom file
|
||||
aFile = xbmcvfs.File(self.jsonFile,'w')
|
||||
# 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
|
||||
# read in the custom file
|
||||
aFile = xbmcvfs.File(self.jsonFile)
|
||||
|
||||
#load custom dirs
|
||||
# load custom dirs
|
||||
self.paths = json.loads(aFile.read())
|
||||
aFile.close()
|
||||
else:
|
||||
#write a blank file
|
||||
# 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 _cleanPath(self, root, path):
|
||||
return path[len(root) - 1:]
|
||||
|
||||
def _validatePath(self,root,path):
|
||||
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):
|
||||
name = self.dialog.input(utils.getString(30110), defaultt='Backup Set')
|
||||
|
||||
#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))
|
||||
if(name is not None):
|
||||
|
||||
# give a choice to start in home or enter a root path
|
||||
enterHome = self.dialog.yesno(utils.getString(30111), message=utils.getString(30112) + " - " + utils.getString(30114) + "\n" + 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)
|
||||
rootFolder = self.dialog.input(utils.getString(30116), defaultt=rootFolder)
|
||||
|
||||
#direcotry has to end in slash
|
||||
# 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)
|
||||
# check that this path even exists
|
||||
if(not xbmcvfs.exists(xbmcvfs.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)
|
||||
# select path to start set
|
||||
rootFolder = self.dialog.browse(type=0, heading=utils.getString(30119), shares='files', defaultt=rootFolder)
|
||||
|
||||
backupSet = {'name': name, 'root': rootFolder}
|
||||
|
||||
backupSet = {'name':name,'root':rootFolder}
|
||||
|
||||
return backupSet
|
||||
|
||||
def editSet(self,name,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))]
|
||||
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)
|
||||
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'])
|
||||
|
||||
# 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
|
||||
# cannot add root as an exclusion
|
||||
if(optionSelected == 0 and addFolder != backupSet['root']):
|
||||
backupSet['dirs'].append({"path":addFolder,"type":"exclude"})
|
||||
backupSet['dirs'].append({"path": addFolder, "type": "exclude"})
|
||||
elif(optionSelected == 1):
|
||||
#can add root as inclusion
|
||||
backupSet['dirs'].append({"path":addFolder,"type":"include","recurse":True})
|
||||
# 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)
|
||||
# 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)
|
||||
# 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'])
|
||||
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')
|
||||
|
||||
cOptions.append(utils.getString(30147))
|
||||
|
||||
contextOption = self.dialog.contextmenu(cOptions)
|
||||
|
||||
|
||||
if(contextOption == 0):
|
||||
if(self.dialog.yesno(heading=utils.getString(30123),line1=utils.getString(30128))):
|
||||
#remove folder
|
||||
if(self.dialog.yesno(heading=utils.getString(30123), message=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
|
||||
# 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))
|
||||
|
||||
# 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())):
|
||||
# load the custom paths
|
||||
listItem = xbmcgui.ListItem(utils.getString(30126), '')
|
||||
listItem.setArt({'icon': os.path.join(utils.addon_dir(), 'resources', 'images', 'plus-icon.png')})
|
||||
options = [listItem]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
listItem = xbmcgui.ListItem(aSet['name'], utils.getString(30121) + ': ' + aSet['set']['root'])
|
||||
listItem.setArt({'icon': os.path.join(utils.addon_dir(), 'resources', 'images', 'folder-icon.png')})
|
||||
options.append(listItem)
|
||||
|
||||
# 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
|
||||
# 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'])
|
||||
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)])
|
||||
# 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'])
|
||||
# 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)
|
||||
|
||||
#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)
|
||||
if(self.dialog.yesno(heading=utils.getString(30127), message=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))
|
||||
# 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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,68 +1,63 @@
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
import resources.lib.tinyurl as tinyurl
|
||||
import resources.lib.utils as utils
|
||||
|
||||
#don't die on import error yet, these might not even get used
|
||||
# don't die on import error yet, these might not even get used
|
||||
try:
|
||||
import dropbox
|
||||
from dropbox import dropbox
|
||||
from dropbox import oauth
|
||||
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 = ""
|
||||
APP_SECRET = ""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.APP_KEY = utils.getSetting('dropbox_key')
|
||||
self.APP_SECRET = utils.getSetting('dropbox_secret')
|
||||
|
||||
def setup(self):
|
||||
result = True
|
||||
|
||||
|
||||
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))
|
||||
# we can't go any farther, need these for sure
|
||||
xbmcgui.Dialog().ok(utils.getString(30010), '%s %s\n%s' % (utils.getString(30027), utils.getString(30058), utils.getString(30059)))
|
||||
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
def isAuthorized(self):
|
||||
user_token = self._getToken()
|
||||
|
||||
return user_token != ''
|
||||
return user_token != ''
|
||||
|
||||
def authorize(self):
|
||||
result = True
|
||||
|
||||
if(not self.setup()):
|
||||
return False
|
||||
|
||||
|
||||
if(self.isAuthorized()):
|
||||
#delete the token to start over
|
||||
# delete the token to start over
|
||||
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)
|
||||
# copied flow from http://dropbox-sdk-python.readthedocs.io/en/latest/moduledoc.html#dropbox.oauth.DropboxOAuth2FlowNoRedirect
|
||||
flow = oauth.DropboxOAuth2FlowNoRedirect(self.APP_KEY, self.APP_SECRET)
|
||||
|
||||
url = flow.start()
|
||||
|
||||
#print url in log
|
||||
# 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
|
||||
# get the auth code
|
||||
code = xbmcgui.Dialog().input(utils.getString(30027) + ' ' + utils.getString(30103))
|
||||
|
||||
#if user authorized this will work
|
||||
|
||||
# if user authorized this will work
|
||||
|
||||
try:
|
||||
user_token = flow.finish(code)
|
||||
@@ -70,104 +65,45 @@ class DropboxAuthorizer:
|
||||
except Exception as e:
|
||||
utils.log("Error: %s" % (e,))
|
||||
result = False
|
||||
|
||||
return result;
|
||||
|
||||
#return the DropboxClient, or None if can't be created
|
||||
return result
|
||||
|
||||
# return the DropboxClient, or None if can't be created
|
||||
def getClient(self):
|
||||
result = None
|
||||
|
||||
user_token = self._getToken()
|
||||
|
||||
if(user_token != ''):
|
||||
#create the client
|
||||
# create the client
|
||||
result = dropbox.Dropbox(user_token)
|
||||
|
||||
try:
|
||||
result.users_get_current_account()
|
||||
except:
|
||||
#this didn't work, delete the token file
|
||||
# this didn't work, delete the token file
|
||||
self._deleteToken()
|
||||
result = None
|
||||
|
||||
|
||||
return result
|
||||
|
||||
def _setToken(self,token):
|
||||
#write the token files
|
||||
token_file = open(xbmc.translatePath(utils.data_dir() + "tokens.txt"),'w')
|
||||
def _setToken(self, token):
|
||||
# write the token files
|
||||
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"))
|
||||
# get token, if it exists
|
||||
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()
|
||||
|
||||
return token
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def _deleteToken(self):
|
||||
if(xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "tokens.txt"))):
|
||||
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
|
||||
|
||||
class GoogleDriveAuthorizer:
|
||||
CLIENT_ID = ''
|
||||
CLIENT_SECRET = ''
|
||||
|
||||
def __init__(self):
|
||||
self.CLIENT_ID = utils.getSetting('google_drive_id')
|
||||
self.CLIENT_SECRET = utils.getSetting('google_drive_secret')
|
||||
|
||||
def setup(self):
|
||||
result = True
|
||||
|
||||
if(self.CLIENT_ID == '' and self.CLIENT_SECRET == ''):
|
||||
#we can't go any farther, need these for sure
|
||||
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30098) + ' ' + utils.getString(30058),utils.getString(30108))
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def isAuthorized(self):
|
||||
return xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "google_drive.dat"))
|
||||
|
||||
def authorize(self):
|
||||
result = True
|
||||
|
||||
if(not self.setup()):
|
||||
return False
|
||||
|
||||
#create authorization helper and load default settings
|
||||
gauth = GoogleAuth(xbmc.validatePath(xbmc.translatePath(utils.addon_dir() + '/resources/lib/pydrive/settings.yaml')))
|
||||
gauth.LoadClientConfigSettings()
|
||||
|
||||
settings = {"client_id":self.CLIENT_ID,'client_secret':self.CLIENT_SECRET}
|
||||
|
||||
drive_url = gauth.GetAuthUrl(settings)
|
||||
|
||||
utils.log("Google Drive Authorize URL: " + drive_url)
|
||||
|
||||
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30056),utils.getString(30102),tinyurl.shorten(drive_url))
|
||||
code = xbmcgui.Dialog().input(utils.getString(30098) + ' ' + utils.getString(30103))
|
||||
|
||||
gauth.Auth(code)
|
||||
gauth.SaveCredentialsFile(xbmc.validatePath(xbmc.translatePath(utils.data_dir() + 'google_drive.dat')))
|
||||
|
||||
return result
|
||||
|
||||
def getClient(self):
|
||||
#create authorization helper and load default settings
|
||||
gauth = GoogleAuth(xbmc.validatePath(xbmc.translatePath(utils.addon_dir() + '/resources/lib/pydrive/settings.yaml')))
|
||||
gauth.LoadClientConfigSettings()
|
||||
|
||||
gauth.LoadCredentialsFile(xbmc.validatePath(xbmc.translatePath(utils.data_dir() + 'google_drive.dat')))
|
||||
|
||||
result = GoogleDrive(gauth)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
if(xbmcvfs.exists(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))):
|
||||
xbmcvfs.delete(xbmcvfs.translatePath(utils.data_dir() + "tokens.txt"))
|
||||
|
||||
@@ -1,374 +1,392 @@
|
||||
from __future__ import unicode_literals
|
||||
import time
|
||||
import json
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
import utils as utils
|
||||
import time
|
||||
import json
|
||||
import os.path
|
||||
from . import utils as utils
|
||||
from datetime import datetime
|
||||
from vfs import XBMCFileSystem,DropboxFileSystem,ZipFileSystem,GoogleDriveFilesystem
|
||||
from progressbar import BackupProgressBar
|
||||
from . vfs import XBMCFileSystem, DropboxFileSystem, ZipFileSystem
|
||||
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
|
||||
# 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']
|
||||
ZIP_TEMP_PATH = None
|
||||
|
||||
#file systems
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
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.ZIP_TEMP_PATH = xbmcvfs.translatePath(utils.getSetting('zip_temp_path'))
|
||||
|
||||
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_base_path = utils.getSetting('remote_path_2')
|
||||
self.remote_vfs = XBMCFileSystem(utils.getSetting('remote_path_2'))
|
||||
utils.setSetting("remote_path","")
|
||||
utils.setSetting("remote_path", "")
|
||||
elif(utils.getSetting('remote_selection') == '0'):
|
||||
self.remote_base_path = utils.getSetting('remote_path');
|
||||
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 == ""):
|
||||
if(self.remote_base_path == "" or not xbmcvfs.exists(self.ZIP_TEMP_PATH)):
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
#reverse - should reverse the resulting, default is true - newest to oldest
|
||||
def listBackups(self,reverse=True):
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
# format the name according to regional settings
|
||||
folderName = self._dateFormat(aDir)
|
||||
|
||||
result.append((aDir,folderName))
|
||||
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)
|
||||
folderName = aFile.split('.')[0]
|
||||
|
||||
result.append((aFile ,folderName))
|
||||
|
||||
if(file_ext == 'zip' and len(folderName) == 12 and folderName.isdigit()):
|
||||
|
||||
# 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))
|
||||
|
||||
result.sort(key=folderSort, reverse=reverse)
|
||||
|
||||
result.sort(key=folderSort,reverse=reverse)
|
||||
|
||||
return result
|
||||
|
||||
def selectRestore(self,restore_point):
|
||||
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)
|
||||
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
|
||||
# check if remote path exists
|
||||
if(self.remote_vfs.exists(self.remote_vfs.root_path)):
|
||||
#may be data in here already
|
||||
# may be data in here already
|
||||
utils.log(utils.getString(30050))
|
||||
else:
|
||||
#make the remote directory
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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']))
|
||||
# if this dir enabled
|
||||
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:
|
||||
#advanced mode - load custom paths
|
||||
# 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
|
||||
# get the set names
|
||||
keys = list(selectedDirs.keys())
|
||||
|
||||
# go through the custom sets
|
||||
for aKey in keys:
|
||||
#get the set
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
# we may not be able to write to this destination for some reason
|
||||
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30089), "%s\n%s" % (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
|
||||
|
||||
# backup all the files
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# 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'):
|
||||
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.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
|
||||
# send the zip file to the real remote vfs
|
||||
zip_name = os.path.join(self.ZIP_TEMP_PATH, self.remote_vfs.root_path[:-1] + ".zip")
|
||||
self.remote_vfs.cleanup()
|
||||
self.xbmc_vfs.rename(os.path.join(self.ZIP_TEMP_PATH, "xbmc_backup_temp.zip"), zip_name)
|
||||
fileManager.addFile(zip_name)
|
||||
|
||||
# set root to data dir home and reset remote
|
||||
self.xbmc_vfs.set_root(self.ZIP_TEMP_PATH)
|
||||
self.remote_vfs = self.saved_remote_vfs
|
||||
|
||||
# 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), '%s\n%s' % (utils.getString(30090), utils.getString(30091)))
|
||||
|
||||
# delete the temp zip file
|
||||
self.xbmc_vfs.rmfile(zip_name)
|
||||
|
||||
# remove old backups
|
||||
self._rotateBackups()
|
||||
|
||||
#close any files
|
||||
|
||||
# close any files
|
||||
self._closeVFS()
|
||||
|
||||
def restore(self,progressOverride=False,selectedSets=None):
|
||||
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
|
||||
# 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
|
||||
|
||||
# set root to data dir home
|
||||
self.xbmc_vfs.set_root(self.ZIP_TEMP_PATH)
|
||||
restore_path = os.path.join(self.ZIP_TEMP_PATH, self.restore_point)
|
||||
if(not self.xbmc_vfs.exists(restore_path)):
|
||||
# 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)
|
||||
|
||||
self._copyFiles(zipFile,self.remote_vfs, self.xbmc_vfs)
|
||||
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')
|
||||
|
||||
# extract the zip file
|
||||
zip_vfs = ZipFileSystem(restore_path, 'r')
|
||||
extractor = ZipExtractor()
|
||||
|
||||
if(not extractor.extract(zip_vfs, xbmc.translatePath("special://temp/"), self.progressBar)):
|
||||
#we had a problem extracting the archive, delete everything
|
||||
|
||||
if(not extractor.extract(zip_vfs, self.ZIP_TEMP_PATH, 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))
|
||||
self.xbmc_vfs.rmfile(restore_path)
|
||||
|
||||
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
|
||||
|
||||
self.progressBar.updateProgress(0, utils.getString(30049) + "......")
|
||||
# set the new remote vfs and fix xbmc path
|
||||
self.remote_vfs = XBMCFileSystem(os.path.join(self.ZIP_TEMP_PATH, 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)
|
||||
if(valFile == None):
|
||||
#don't continue
|
||||
if(valFile is None):
|
||||
# don't continue
|
||||
return
|
||||
|
||||
utils.log(utils.getString(30051))
|
||||
allFiles = []
|
||||
fileManager = FileManager(self.remote_vfs)
|
||||
|
||||
#check for the existance of an advancedsettings file
|
||||
|
||||
# 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))
|
||||
# let the user know there is an advanced settings file present
|
||||
restartXbmc = xbmcgui.Dialog().yesno(utils.getString(30038), "%s\n%s" % (utils.getString(30039), utils.getString(30040)), utils.getString(30041))
|
||||
|
||||
if(restartXbmc):
|
||||
#add only this file to the file list
|
||||
# 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)
|
||||
self._copyFiles(fileManager.getFiles(), self.remote_vfs, self.xbmc_vfs)
|
||||
|
||||
#let the service know to resume this backup on startup
|
||||
# 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))
|
||||
# do not continue running
|
||||
xbmcgui.Dialog().ok(utils.getString(30077), utils.getString(30078))
|
||||
return
|
||||
|
||||
#use a multiselect dialog to select sets to restore
|
||||
# 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']]
|
||||
|
||||
#if passed in list, skip selection
|
||||
if(selectedSets == None):
|
||||
selectedSets = xbmcgui.Dialog().multiselect(utils.getString(30131),restoreSets)
|
||||
# if passed in list, skip selection
|
||||
if(selectedSets is 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
|
||||
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
|
||||
# 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
|
||||
# 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()})
|
||||
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
|
||||
# restore all the files
|
||||
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)
|
||||
self._copyFiles(fileGroup['files'], self.remote_vfs, self.xbmc_vfs)
|
||||
|
||||
self.progressBar.updateProgress(99,"Clean up operations .....")
|
||||
# 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.rmdir(self.remote_vfs.root_path)
|
||||
# delete the zip file and the extracted directory
|
||||
self.xbmc_vfs.rmfile(os.path.join(self.ZIP_TEMP_PATH, self.restore_point))
|
||||
xbmc.sleep(1000)
|
||||
self.xbmc_vfs.rmdir(self.remote_vfs.clean_path(os.path.join(self.ZIP_TEMP_PATH, self.restore_point.split(".")[0])))
|
||||
xbmc.sleep(1000)
|
||||
|
||||
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
|
||||
# call update addons to refresh everything
|
||||
xbmc.executebuiltin('UpdateLocalAddons')
|
||||
|
||||
def _setupVFS(self,mode=-1,progressOverride=False):
|
||||
#set windows setting to true
|
||||
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
|
||||
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))
|
||||
if(utils.getSettingBool("compress_backups")):
|
||||
# delete old temp file
|
||||
zip_path = os.path.join(self.ZIP_TEMP_PATH, 'xbmc_backup_temp.zip')
|
||||
if(self.xbmc_vfs.exists(zip_path)):
|
||||
if(not self.xbmc_vfs.rmfile(zip_path)):
|
||||
# we had some kind of error deleting the old file
|
||||
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
|
||||
|
||||
# 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(zip_path, "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 != ''):
|
||||
elif(mode == self.Restore and self.restore_point is not 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
|
||||
# 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)
|
||||
utils.log(utils.getString(30152) + ": " + utils.getSetting('zip_temp_path'))
|
||||
|
||||
|
||||
#setup the progress bar
|
||||
# setup the progress bar
|
||||
self.progressBar = BackupProgressBar(progressOverride)
|
||||
self.progressBar.create(progressBarTitle,utils.getString(30049) + "......")
|
||||
self.progressBar.create(progressBarTitle, utils.getString(30049) + "......")
|
||||
|
||||
#if we made it this far we're good
|
||||
# if we made it this far we're good
|
||||
return True
|
||||
|
||||
def _closeVFS(self):
|
||||
@@ -376,241 +394,253 @@ class XbmcBackup:
|
||||
self.remote_vfs.cleanup()
|
||||
self.progressBar.close()
|
||||
|
||||
#reset the window setting
|
||||
# reset the window setting
|
||||
window = xbmcgui.Window(10000)
|
||||
window.setProperty(utils.__addon_id__ + ".running","")
|
||||
window.setProperty(utils.__addon_id__ + ".running", "")
|
||||
|
||||
def _copyFiles(self,fileList,source,dest):
|
||||
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
|
||||
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)):
|
||||
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:])
|
||||
if(utils.getSettingBool('verbose_logging')):
|
||||
utils.log('Writing file: ' + aFile['file'])
|
||||
|
||||
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()
|
||||
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
|
||||
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):
|
||||
result = False
|
||||
|
||||
|
||||
|
||||
return result
|
||||
|
||||
def _addBackupDir(self,folder_name,root_path,dirList):
|
||||
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
|
||||
# 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'])
|
||||
|
||||
# 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()}
|
||||
|
||||
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 _updateProgress(self, message=None):
|
||||
self.progressBar.updateProgress(int((float(self.transferSize - self.transferLeft) / float(self.transferSize)) * 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
|
||||
total_backups = utils.getSettingInt('backup_rotation')
|
||||
|
||||
#update the progress bar if it is available
|
||||
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
|
||||
|
||||
# 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])
|
||||
# this is a file, remove it that way
|
||||
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}
|
||||
def _createValidationFile(self, dirList):
|
||||
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']})
|
||||
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")
|
||||
|
||||
#remove the validation file
|
||||
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "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(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')
|
||||
# android requires a .nomedia file to not index the directory as media
|
||||
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
|
||||
|
||||
def _checkValidationFile(self,path):
|
||||
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()
|
||||
# copy the file and open it
|
||||
self._copyFile(self.remote_vfs, self.xbmc_vfs, path + "xbmcbackup.val", xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
|
||||
|
||||
#delete after checking
|
||||
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "xbmcbackup_restore.val"))
|
||||
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "xbmcbackup_restore.val"), 'r') as vFile:
|
||||
jsonString = vFile.read()
|
||||
|
||||
# delete after checking
|
||||
xbmcvfs.delete(xbmcvfs.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))
|
||||
shouldContinue = xbmcgui.Dialog().yesno(utils.getString(30085), "%s\n%s" % (utils.getString(30086), utils.getString(30044)))
|
||||
|
||||
if(not shouldContinue):
|
||||
result = None
|
||||
|
||||
|
||||
except ValueError:
|
||||
#may fail on older archives
|
||||
# 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()
|
||||
with xbmcvfs.File(xbmcvfs.translatePath(utils.data_dir() + "resume.txt"), 'w') as f:
|
||||
f.write(self.restore_point)
|
||||
|
||||
def _readBackupConfig(self,aFile):
|
||||
jFile = xbmcvfs.File(xbmc.translatePath(aFile),'r')
|
||||
jsonString = jFile.read()
|
||||
jFile.close()
|
||||
def _readBackupConfig(self, aFile):
|
||||
with xbmcvfs.File(xbmcvfs.translatePath(aFile), 'r') as f:
|
||||
jsonString = f.read()
|
||||
return json.loads(jsonString)
|
||||
|
||||
|
||||
class FileManager:
|
||||
not_dir = ['.zip','.xsp','.rar']
|
||||
not_dir = ['.zip', '.xsp', '.rar']
|
||||
exclude_dir = []
|
||||
root_dirs = []
|
||||
|
||||
def __init__(self,vfs):
|
||||
pathSep = '/'
|
||||
totalSize = 0
|
||||
|
||||
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))
|
||||
for aDir in self.root_dirs:
|
||||
self.addFile('-' + xbmcvfs.translatePath(aDir['path']))
|
||||
self.walkTree(xbmcvfs.translatePath(aDir['path']), aDir['recurse'])
|
||||
|
||||
def walkTree(self, directory, recurse=True):
|
||||
if(utils.getSettingBool('verbose_logging')):
|
||||
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(self.vfs.exists(directory + self.pathSep)):
|
||||
dirs, files = self.vfs.listdir(directory)
|
||||
|
||||
if(recurse):
|
||||
#create all the subdirs first
|
||||
# create all the subdirs first
|
||||
for aDir in dirs:
|
||||
dirPath = xbmc.validatePath(xbmc.translatePath(directory + "/" + aDir))
|
||||
dirPath = xbmcvfs.validatePath(xbmcvfs.translatePath(directory + self.pathSep + aDir))
|
||||
file_ext = aDir.split('.')[-1]
|
||||
|
||||
#check if directory is excluded
|
||||
# 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
|
||||
# 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
|
||||
self.walkTree(dirPath)
|
||||
|
||||
# copy all the files
|
||||
for aFile in files:
|
||||
filePath = xbmc.translatePath(directory + "/" + aFile)
|
||||
filePath = xbmcvfs.translatePath(directory + self.pathSep + aFile)
|
||||
self.addFile(filePath)
|
||||
|
||||
def addDir(self,dirMeta):
|
||||
def addDir(self, dirMeta):
|
||||
if(dirMeta['type'] == 'include'):
|
||||
self.root_dirs.append({'path':dirMeta['path'],'recurse':dirMeta['recurse']})
|
||||
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)
|
||||
self.excludeFile(xbmcvfs.translatePath(dirMeta['path']))
|
||||
|
||||
def excludeFile(self,filename):
|
||||
try:
|
||||
filename = filename.decode('UTF-8')
|
||||
except UnicodeDecodeError:
|
||||
filename = filename.decode('ISO-8859-2')
|
||||
|
||||
#remove trailing slash
|
||||
def addFile(self, filename):
|
||||
# write the full remote path name of this file
|
||||
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]
|
||||
|
||||
#write the full remote path name of this file
|
||||
|
||||
# write the full remote path name of this file
|
||||
utils.log("Exclude File: " + filename)
|
||||
self.exclude_dir.append(filename)
|
||||
|
||||
@@ -619,8 +649,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
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
import re
|
||||
from time import time, mktime
|
||||
from datetime import datetime, date
|
||||
from .relativedelta import relativedelta
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
search_re = re.compile(r'^([^-]+)-([^-/]+)(/(.*))?$')
|
||||
only_int_re = re.compile(r'^\d+$')
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .dropbox import __version__, Dropbox, DropboxTeam, create_session # noqa: F401
|
||||
from .oauth import DropboxOAuth2Flow, DropboxOAuth2FlowNoRedirect # noqa: F401
|
||||
@@ -1,310 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
class LaunchResultBase(bb.Union):
|
||||
"""
|
||||
Result returned by methods that launch an asynchronous job. A method who may
|
||||
either launch an asynchronous job, or complete the request synchronously,
|
||||
can use this union by extending it, and adding a 'complete' field with the
|
||||
type of the synchronous response. See :class:`LaunchEmptyResult` for an
|
||||
example.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar str async_job_id: This response indicates that the processing is
|
||||
asynchronous. The string is an id that can be used to obtain the status
|
||||
of the asynchronous job.
|
||||
"""
|
||||
|
||||
_catch_all = None
|
||||
|
||||
@classmethod
|
||||
def async_job_id(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``async_job_id`` tag with
|
||||
value ``val``.
|
||||
|
||||
:param str val:
|
||||
:rtype: LaunchResultBase
|
||||
"""
|
||||
return cls('async_job_id', val)
|
||||
|
||||
def is_async_job_id(self):
|
||||
"""
|
||||
Check if the union tag is ``async_job_id``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'async_job_id'
|
||||
|
||||
def get_async_job_id(self):
|
||||
"""
|
||||
This response indicates that the processing is asynchronous. The string
|
||||
is an id that can be used to obtain the status of the asynchronous job.
|
||||
|
||||
Only call this if :meth:`is_async_job_id` is true.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.is_async_job_id():
|
||||
raise AttributeError("tag 'async_job_id' not set")
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return 'LaunchResultBase(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
LaunchResultBase_validator = bv.Union(LaunchResultBase)
|
||||
|
||||
class LaunchEmptyResult(LaunchResultBase):
|
||||
"""
|
||||
Result returned by methods that may either launch an asynchronous job or
|
||||
complete synchronously. Upon synchronous completion of the job, no
|
||||
additional information is returned.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar complete: The job finished synchronously and successfully.
|
||||
"""
|
||||
|
||||
# Attribute is overwritten below the class definition
|
||||
complete = None
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
Check if the union tag is ``complete``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'complete'
|
||||
|
||||
def __repr__(self):
|
||||
return 'LaunchEmptyResult(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
LaunchEmptyResult_validator = bv.Union(LaunchEmptyResult)
|
||||
|
||||
class PollArg(object):
|
||||
"""
|
||||
Arguments for methods that poll the status of an asynchronous job.
|
||||
|
||||
:ivar async_job_id: Id of the asynchronous job. This is the value of a
|
||||
response returned from the method that launched the job.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_async_job_id_value',
|
||||
'_async_job_id_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
async_job_id=None):
|
||||
self._async_job_id_value = None
|
||||
self._async_job_id_present = False
|
||||
if async_job_id is not None:
|
||||
self.async_job_id = async_job_id
|
||||
|
||||
@property
|
||||
def async_job_id(self):
|
||||
"""
|
||||
Id of the asynchronous job. This is the value of a response returned
|
||||
from the method that launched the job.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._async_job_id_present:
|
||||
return self._async_job_id_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'async_job_id'")
|
||||
|
||||
@async_job_id.setter
|
||||
def async_job_id(self, val):
|
||||
val = self._async_job_id_validator.validate(val)
|
||||
self._async_job_id_value = val
|
||||
self._async_job_id_present = True
|
||||
|
||||
@async_job_id.deleter
|
||||
def async_job_id(self):
|
||||
self._async_job_id_value = None
|
||||
self._async_job_id_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'PollArg(async_job_id={!r})'.format(
|
||||
self._async_job_id_value,
|
||||
)
|
||||
|
||||
PollArg_validator = bv.Struct(PollArg)
|
||||
|
||||
class PollResultBase(bb.Union):
|
||||
"""
|
||||
Result returned by methods that poll for the status of an asynchronous job.
|
||||
Unions that extend this union should add a 'complete' field with a type of
|
||||
the information returned upon job completion. See :class:`PollEmptyResult`
|
||||
for an example.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar in_progress: The asynchronous job is still in progress.
|
||||
"""
|
||||
|
||||
_catch_all = None
|
||||
# Attribute is overwritten below the class definition
|
||||
in_progress = None
|
||||
|
||||
def is_in_progress(self):
|
||||
"""
|
||||
Check if the union tag is ``in_progress``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'in_progress'
|
||||
|
||||
def __repr__(self):
|
||||
return 'PollResultBase(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PollResultBase_validator = bv.Union(PollResultBase)
|
||||
|
||||
class PollEmptyResult(PollResultBase):
|
||||
"""
|
||||
Result returned by methods that poll for the status of an asynchronous job.
|
||||
Upon completion of the job, no additional information is returned.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar complete: The asynchronous job has completed successfully.
|
||||
"""
|
||||
|
||||
# Attribute is overwritten below the class definition
|
||||
complete = None
|
||||
|
||||
def is_complete(self):
|
||||
"""
|
||||
Check if the union tag is ``complete``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'complete'
|
||||
|
||||
def __repr__(self):
|
||||
return 'PollEmptyResult(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PollEmptyResult_validator = bv.Union(PollEmptyResult)
|
||||
|
||||
class PollError(bb.Union):
|
||||
"""
|
||||
Error returned by methods for polling the status of asynchronous job.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar invalid_async_job_id: The job ID is invalid.
|
||||
:ivar internal_error: Something went wrong with the job on Dropbox's end.
|
||||
You'll need to verify that the action you were taking succeeded, and if
|
||||
not, try again. This should happen very rarely.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
invalid_async_job_id = None
|
||||
# Attribute is overwritten below the class definition
|
||||
internal_error = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_invalid_async_job_id(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_async_job_id``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_async_job_id'
|
||||
|
||||
def is_internal_error(self):
|
||||
"""
|
||||
Check if the union tag is ``internal_error``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'internal_error'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'PollError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PollError_validator = bv.Union(PollError)
|
||||
|
||||
AsyncJobId_validator = bv.String(min_length=1)
|
||||
LaunchResultBase._async_job_id_validator = AsyncJobId_validator
|
||||
LaunchResultBase._tagmap = {
|
||||
'async_job_id': LaunchResultBase._async_job_id_validator,
|
||||
}
|
||||
|
||||
LaunchEmptyResult._complete_validator = bv.Void()
|
||||
LaunchEmptyResult._tagmap = {
|
||||
'complete': LaunchEmptyResult._complete_validator,
|
||||
}
|
||||
LaunchEmptyResult._tagmap.update(LaunchResultBase._tagmap)
|
||||
|
||||
LaunchEmptyResult.complete = LaunchEmptyResult('complete')
|
||||
|
||||
PollArg._async_job_id_validator = AsyncJobId_validator
|
||||
PollArg._all_field_names_ = set(['async_job_id'])
|
||||
PollArg._all_fields_ = [('async_job_id', PollArg._async_job_id_validator)]
|
||||
|
||||
PollResultBase._in_progress_validator = bv.Void()
|
||||
PollResultBase._tagmap = {
|
||||
'in_progress': PollResultBase._in_progress_validator,
|
||||
}
|
||||
|
||||
PollResultBase.in_progress = PollResultBase('in_progress')
|
||||
|
||||
PollEmptyResult._complete_validator = bv.Void()
|
||||
PollEmptyResult._tagmap = {
|
||||
'complete': PollEmptyResult._complete_validator,
|
||||
}
|
||||
PollEmptyResult._tagmap.update(PollResultBase._tagmap)
|
||||
|
||||
PollEmptyResult.complete = PollEmptyResult('complete')
|
||||
|
||||
PollError._invalid_async_job_id_validator = bv.Void()
|
||||
PollError._internal_error_validator = bv.Void()
|
||||
PollError._other_validator = bv.Void()
|
||||
PollError._tagmap = {
|
||||
'invalid_async_job_id': PollError._invalid_async_job_id_validator,
|
||||
'internal_error': PollError._internal_error_validator,
|
||||
'other': PollError._other_validator,
|
||||
}
|
||||
|
||||
PollError.invalid_async_job_id = PollError('invalid_async_job_id')
|
||||
PollError.internal_error = PollError('internal_error')
|
||||
PollError.other = PollError('other')
|
||||
|
||||
ROUTES = {
|
||||
}
|
||||
|
||||
@@ -1,723 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
class AccessError(bb.Union):
|
||||
"""
|
||||
Error occurred because the account doesn't have permission to access the
|
||||
resource.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar InvalidAccountTypeError invalid_account_type: Current account type
|
||||
cannot access the resource.
|
||||
:ivar PaperAccessError paper_access_denied: Current account cannot access
|
||||
Paper.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
@classmethod
|
||||
def invalid_account_type(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``invalid_account_type`` tag
|
||||
with value ``val``.
|
||||
|
||||
:param InvalidAccountTypeError val:
|
||||
:rtype: AccessError
|
||||
"""
|
||||
return cls('invalid_account_type', val)
|
||||
|
||||
@classmethod
|
||||
def paper_access_denied(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``paper_access_denied`` tag
|
||||
with value ``val``.
|
||||
|
||||
:param PaperAccessError val:
|
||||
:rtype: AccessError
|
||||
"""
|
||||
return cls('paper_access_denied', val)
|
||||
|
||||
def is_invalid_account_type(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_account_type``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_account_type'
|
||||
|
||||
def is_paper_access_denied(self):
|
||||
"""
|
||||
Check if the union tag is ``paper_access_denied``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'paper_access_denied'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def get_invalid_account_type(self):
|
||||
"""
|
||||
Current account type cannot access the resource.
|
||||
|
||||
Only call this if :meth:`is_invalid_account_type` is true.
|
||||
|
||||
:rtype: InvalidAccountTypeError
|
||||
"""
|
||||
if not self.is_invalid_account_type():
|
||||
raise AttributeError("tag 'invalid_account_type' not set")
|
||||
return self._value
|
||||
|
||||
def get_paper_access_denied(self):
|
||||
"""
|
||||
Current account cannot access Paper.
|
||||
|
||||
Only call this if :meth:`is_paper_access_denied` is true.
|
||||
|
||||
:rtype: PaperAccessError
|
||||
"""
|
||||
if not self.is_paper_access_denied():
|
||||
raise AttributeError("tag 'paper_access_denied' not set")
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return 'AccessError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
AccessError_validator = bv.Union(AccessError)
|
||||
|
||||
class AuthError(bb.Union):
|
||||
"""
|
||||
Errors occurred during authentication.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar invalid_access_token: The access token is invalid.
|
||||
:ivar invalid_select_user: The user specified in 'Dropbox-API-Select-User'
|
||||
is no longer on the team.
|
||||
:ivar invalid_select_admin: The user specified in 'Dropbox-API-Select-Admin'
|
||||
is not a Dropbox Business team admin.
|
||||
:ivar user_suspended: The user has been suspended.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
invalid_access_token = None
|
||||
# Attribute is overwritten below the class definition
|
||||
invalid_select_user = None
|
||||
# Attribute is overwritten below the class definition
|
||||
invalid_select_admin = None
|
||||
# Attribute is overwritten below the class definition
|
||||
user_suspended = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_invalid_access_token(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_access_token``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_access_token'
|
||||
|
||||
def is_invalid_select_user(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_select_user``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_select_user'
|
||||
|
||||
def is_invalid_select_admin(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_select_admin``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_select_admin'
|
||||
|
||||
def is_user_suspended(self):
|
||||
"""
|
||||
Check if the union tag is ``user_suspended``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'user_suspended'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'AuthError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
AuthError_validator = bv.Union(AuthError)
|
||||
|
||||
class InvalidAccountTypeError(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar endpoint: Current account type doesn't have permission to access this
|
||||
route endpoint.
|
||||
:ivar feature: Current account type doesn't have permission to access this
|
||||
feature.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
endpoint = None
|
||||
# Attribute is overwritten below the class definition
|
||||
feature = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_endpoint(self):
|
||||
"""
|
||||
Check if the union tag is ``endpoint``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'endpoint'
|
||||
|
||||
def is_feature(self):
|
||||
"""
|
||||
Check if the union tag is ``feature``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'feature'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'InvalidAccountTypeError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
InvalidAccountTypeError_validator = bv.Union(InvalidAccountTypeError)
|
||||
|
||||
class PaperAccessError(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar paper_disabled: Paper is disabled.
|
||||
:ivar not_paper_user: The provided user has not used Paper yet.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
paper_disabled = None
|
||||
# Attribute is overwritten below the class definition
|
||||
not_paper_user = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_paper_disabled(self):
|
||||
"""
|
||||
Check if the union tag is ``paper_disabled``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'paper_disabled'
|
||||
|
||||
def is_not_paper_user(self):
|
||||
"""
|
||||
Check if the union tag is ``not_paper_user``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'not_paper_user'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'PaperAccessError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PaperAccessError_validator = bv.Union(PaperAccessError)
|
||||
|
||||
class RateLimitError(object):
|
||||
"""
|
||||
Error occurred because the app is being rate limited.
|
||||
|
||||
:ivar reason: The reason why the app is being rate limited.
|
||||
:ivar retry_after: The number of seconds that the app should wait before
|
||||
making another request.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_reason_value',
|
||||
'_reason_present',
|
||||
'_retry_after_value',
|
||||
'_retry_after_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
reason=None,
|
||||
retry_after=None):
|
||||
self._reason_value = None
|
||||
self._reason_present = False
|
||||
self._retry_after_value = None
|
||||
self._retry_after_present = False
|
||||
if reason is not None:
|
||||
self.reason = reason
|
||||
if retry_after is not None:
|
||||
self.retry_after = retry_after
|
||||
|
||||
@property
|
||||
def reason(self):
|
||||
"""
|
||||
The reason why the app is being rate limited.
|
||||
|
||||
:rtype: RateLimitReason
|
||||
"""
|
||||
if self._reason_present:
|
||||
return self._reason_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'reason'")
|
||||
|
||||
@reason.setter
|
||||
def reason(self, val):
|
||||
self._reason_validator.validate_type_only(val)
|
||||
self._reason_value = val
|
||||
self._reason_present = True
|
||||
|
||||
@reason.deleter
|
||||
def reason(self):
|
||||
self._reason_value = None
|
||||
self._reason_present = False
|
||||
|
||||
@property
|
||||
def retry_after(self):
|
||||
"""
|
||||
The number of seconds that the app should wait before making another
|
||||
request.
|
||||
|
||||
:rtype: long
|
||||
"""
|
||||
if self._retry_after_present:
|
||||
return self._retry_after_value
|
||||
else:
|
||||
return 1
|
||||
|
||||
@retry_after.setter
|
||||
def retry_after(self, val):
|
||||
val = self._retry_after_validator.validate(val)
|
||||
self._retry_after_value = val
|
||||
self._retry_after_present = True
|
||||
|
||||
@retry_after.deleter
|
||||
def retry_after(self):
|
||||
self._retry_after_value = None
|
||||
self._retry_after_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'RateLimitError(reason={!r}, retry_after={!r})'.format(
|
||||
self._reason_value,
|
||||
self._retry_after_value,
|
||||
)
|
||||
|
||||
RateLimitError_validator = bv.Struct(RateLimitError)
|
||||
|
||||
class RateLimitReason(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar too_many_requests: You are making too many requests in the past few
|
||||
minutes.
|
||||
:ivar too_many_write_operations: There are currently too many write
|
||||
operations happening in the user's Dropbox.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
too_many_requests = None
|
||||
# Attribute is overwritten below the class definition
|
||||
too_many_write_operations = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_too_many_requests(self):
|
||||
"""
|
||||
Check if the union tag is ``too_many_requests``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'too_many_requests'
|
||||
|
||||
def is_too_many_write_operations(self):
|
||||
"""
|
||||
Check if the union tag is ``too_many_write_operations``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'too_many_write_operations'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'RateLimitReason(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
RateLimitReason_validator = bv.Union(RateLimitReason)
|
||||
|
||||
class TokenFromOAuth1Arg(object):
|
||||
"""
|
||||
:ivar oauth1_token: The supplied OAuth 1.0 access token.
|
||||
:ivar oauth1_token_secret: The token secret associated with the supplied
|
||||
access token.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_oauth1_token_value',
|
||||
'_oauth1_token_present',
|
||||
'_oauth1_token_secret_value',
|
||||
'_oauth1_token_secret_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
oauth1_token=None,
|
||||
oauth1_token_secret=None):
|
||||
self._oauth1_token_value = None
|
||||
self._oauth1_token_present = False
|
||||
self._oauth1_token_secret_value = None
|
||||
self._oauth1_token_secret_present = False
|
||||
if oauth1_token is not None:
|
||||
self.oauth1_token = oauth1_token
|
||||
if oauth1_token_secret is not None:
|
||||
self.oauth1_token_secret = oauth1_token_secret
|
||||
|
||||
@property
|
||||
def oauth1_token(self):
|
||||
"""
|
||||
The supplied OAuth 1.0 access token.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._oauth1_token_present:
|
||||
return self._oauth1_token_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'oauth1_token'")
|
||||
|
||||
@oauth1_token.setter
|
||||
def oauth1_token(self, val):
|
||||
val = self._oauth1_token_validator.validate(val)
|
||||
self._oauth1_token_value = val
|
||||
self._oauth1_token_present = True
|
||||
|
||||
@oauth1_token.deleter
|
||||
def oauth1_token(self):
|
||||
self._oauth1_token_value = None
|
||||
self._oauth1_token_present = False
|
||||
|
||||
@property
|
||||
def oauth1_token_secret(self):
|
||||
"""
|
||||
The token secret associated with the supplied access token.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._oauth1_token_secret_present:
|
||||
return self._oauth1_token_secret_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'oauth1_token_secret'")
|
||||
|
||||
@oauth1_token_secret.setter
|
||||
def oauth1_token_secret(self, val):
|
||||
val = self._oauth1_token_secret_validator.validate(val)
|
||||
self._oauth1_token_secret_value = val
|
||||
self._oauth1_token_secret_present = True
|
||||
|
||||
@oauth1_token_secret.deleter
|
||||
def oauth1_token_secret(self):
|
||||
self._oauth1_token_secret_value = None
|
||||
self._oauth1_token_secret_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'TokenFromOAuth1Arg(oauth1_token={!r}, oauth1_token_secret={!r})'.format(
|
||||
self._oauth1_token_value,
|
||||
self._oauth1_token_secret_value,
|
||||
)
|
||||
|
||||
TokenFromOAuth1Arg_validator = bv.Struct(TokenFromOAuth1Arg)
|
||||
|
||||
class TokenFromOAuth1Error(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar invalid_oauth1_token_info: Part or all of the OAuth 1.0 access token
|
||||
info is invalid.
|
||||
:ivar app_id_mismatch: The authorized app does not match the app associated
|
||||
with the supplied access token.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
invalid_oauth1_token_info = None
|
||||
# Attribute is overwritten below the class definition
|
||||
app_id_mismatch = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_invalid_oauth1_token_info(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid_oauth1_token_info``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid_oauth1_token_info'
|
||||
|
||||
def is_app_id_mismatch(self):
|
||||
"""
|
||||
Check if the union tag is ``app_id_mismatch``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'app_id_mismatch'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'TokenFromOAuth1Error(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
TokenFromOAuth1Error_validator = bv.Union(TokenFromOAuth1Error)
|
||||
|
||||
class TokenFromOAuth1Result(object):
|
||||
"""
|
||||
:ivar oauth2_token: The OAuth 2.0 token generated from the supplied OAuth
|
||||
1.0 token.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_oauth2_token_value',
|
||||
'_oauth2_token_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
oauth2_token=None):
|
||||
self._oauth2_token_value = None
|
||||
self._oauth2_token_present = False
|
||||
if oauth2_token is not None:
|
||||
self.oauth2_token = oauth2_token
|
||||
|
||||
@property
|
||||
def oauth2_token(self):
|
||||
"""
|
||||
The OAuth 2.0 token generated from the supplied OAuth 1.0 token.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._oauth2_token_present:
|
||||
return self._oauth2_token_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'oauth2_token'")
|
||||
|
||||
@oauth2_token.setter
|
||||
def oauth2_token(self, val):
|
||||
val = self._oauth2_token_validator.validate(val)
|
||||
self._oauth2_token_value = val
|
||||
self._oauth2_token_present = True
|
||||
|
||||
@oauth2_token.deleter
|
||||
def oauth2_token(self):
|
||||
self._oauth2_token_value = None
|
||||
self._oauth2_token_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'TokenFromOAuth1Result(oauth2_token={!r})'.format(
|
||||
self._oauth2_token_value,
|
||||
)
|
||||
|
||||
TokenFromOAuth1Result_validator = bv.Struct(TokenFromOAuth1Result)
|
||||
|
||||
AccessError._invalid_account_type_validator = InvalidAccountTypeError_validator
|
||||
AccessError._paper_access_denied_validator = PaperAccessError_validator
|
||||
AccessError._other_validator = bv.Void()
|
||||
AccessError._tagmap = {
|
||||
'invalid_account_type': AccessError._invalid_account_type_validator,
|
||||
'paper_access_denied': AccessError._paper_access_denied_validator,
|
||||
'other': AccessError._other_validator,
|
||||
}
|
||||
|
||||
AccessError.other = AccessError('other')
|
||||
|
||||
AuthError._invalid_access_token_validator = bv.Void()
|
||||
AuthError._invalid_select_user_validator = bv.Void()
|
||||
AuthError._invalid_select_admin_validator = bv.Void()
|
||||
AuthError._user_suspended_validator = bv.Void()
|
||||
AuthError._other_validator = bv.Void()
|
||||
AuthError._tagmap = {
|
||||
'invalid_access_token': AuthError._invalid_access_token_validator,
|
||||
'invalid_select_user': AuthError._invalid_select_user_validator,
|
||||
'invalid_select_admin': AuthError._invalid_select_admin_validator,
|
||||
'user_suspended': AuthError._user_suspended_validator,
|
||||
'other': AuthError._other_validator,
|
||||
}
|
||||
|
||||
AuthError.invalid_access_token = AuthError('invalid_access_token')
|
||||
AuthError.invalid_select_user = AuthError('invalid_select_user')
|
||||
AuthError.invalid_select_admin = AuthError('invalid_select_admin')
|
||||
AuthError.user_suspended = AuthError('user_suspended')
|
||||
AuthError.other = AuthError('other')
|
||||
|
||||
InvalidAccountTypeError._endpoint_validator = bv.Void()
|
||||
InvalidAccountTypeError._feature_validator = bv.Void()
|
||||
InvalidAccountTypeError._other_validator = bv.Void()
|
||||
InvalidAccountTypeError._tagmap = {
|
||||
'endpoint': InvalidAccountTypeError._endpoint_validator,
|
||||
'feature': InvalidAccountTypeError._feature_validator,
|
||||
'other': InvalidAccountTypeError._other_validator,
|
||||
}
|
||||
|
||||
InvalidAccountTypeError.endpoint = InvalidAccountTypeError('endpoint')
|
||||
InvalidAccountTypeError.feature = InvalidAccountTypeError('feature')
|
||||
InvalidAccountTypeError.other = InvalidAccountTypeError('other')
|
||||
|
||||
PaperAccessError._paper_disabled_validator = bv.Void()
|
||||
PaperAccessError._not_paper_user_validator = bv.Void()
|
||||
PaperAccessError._other_validator = bv.Void()
|
||||
PaperAccessError._tagmap = {
|
||||
'paper_disabled': PaperAccessError._paper_disabled_validator,
|
||||
'not_paper_user': PaperAccessError._not_paper_user_validator,
|
||||
'other': PaperAccessError._other_validator,
|
||||
}
|
||||
|
||||
PaperAccessError.paper_disabled = PaperAccessError('paper_disabled')
|
||||
PaperAccessError.not_paper_user = PaperAccessError('not_paper_user')
|
||||
PaperAccessError.other = PaperAccessError('other')
|
||||
|
||||
RateLimitError._reason_validator = RateLimitReason_validator
|
||||
RateLimitError._retry_after_validator = bv.UInt64()
|
||||
RateLimitError._all_field_names_ = set([
|
||||
'reason',
|
||||
'retry_after',
|
||||
])
|
||||
RateLimitError._all_fields_ = [
|
||||
('reason', RateLimitError._reason_validator),
|
||||
('retry_after', RateLimitError._retry_after_validator),
|
||||
]
|
||||
|
||||
RateLimitReason._too_many_requests_validator = bv.Void()
|
||||
RateLimitReason._too_many_write_operations_validator = bv.Void()
|
||||
RateLimitReason._other_validator = bv.Void()
|
||||
RateLimitReason._tagmap = {
|
||||
'too_many_requests': RateLimitReason._too_many_requests_validator,
|
||||
'too_many_write_operations': RateLimitReason._too_many_write_operations_validator,
|
||||
'other': RateLimitReason._other_validator,
|
||||
}
|
||||
|
||||
RateLimitReason.too_many_requests = RateLimitReason('too_many_requests')
|
||||
RateLimitReason.too_many_write_operations = RateLimitReason('too_many_write_operations')
|
||||
RateLimitReason.other = RateLimitReason('other')
|
||||
|
||||
TokenFromOAuth1Arg._oauth1_token_validator = bv.String(min_length=1)
|
||||
TokenFromOAuth1Arg._oauth1_token_secret_validator = bv.String(min_length=1)
|
||||
TokenFromOAuth1Arg._all_field_names_ = set([
|
||||
'oauth1_token',
|
||||
'oauth1_token_secret',
|
||||
])
|
||||
TokenFromOAuth1Arg._all_fields_ = [
|
||||
('oauth1_token', TokenFromOAuth1Arg._oauth1_token_validator),
|
||||
('oauth1_token_secret', TokenFromOAuth1Arg._oauth1_token_secret_validator),
|
||||
]
|
||||
|
||||
TokenFromOAuth1Error._invalid_oauth1_token_info_validator = bv.Void()
|
||||
TokenFromOAuth1Error._app_id_mismatch_validator = bv.Void()
|
||||
TokenFromOAuth1Error._other_validator = bv.Void()
|
||||
TokenFromOAuth1Error._tagmap = {
|
||||
'invalid_oauth1_token_info': TokenFromOAuth1Error._invalid_oauth1_token_info_validator,
|
||||
'app_id_mismatch': TokenFromOAuth1Error._app_id_mismatch_validator,
|
||||
'other': TokenFromOAuth1Error._other_validator,
|
||||
}
|
||||
|
||||
TokenFromOAuth1Error.invalid_oauth1_token_info = TokenFromOAuth1Error('invalid_oauth1_token_info')
|
||||
TokenFromOAuth1Error.app_id_mismatch = TokenFromOAuth1Error('app_id_mismatch')
|
||||
TokenFromOAuth1Error.other = TokenFromOAuth1Error('other')
|
||||
|
||||
TokenFromOAuth1Result._oauth2_token_validator = bv.String(min_length=1)
|
||||
TokenFromOAuth1Result._all_field_names_ = set(['oauth2_token'])
|
||||
TokenFromOAuth1Result._all_fields_ = [('oauth2_token', TokenFromOAuth1Result._oauth2_token_validator)]
|
||||
|
||||
token_from_oauth1 = bb.Route(
|
||||
'token/from_oauth1',
|
||||
False,
|
||||
TokenFromOAuth1Arg_validator,
|
||||
TokenFromOAuth1Result_validator,
|
||||
TokenFromOAuth1Error_validator,
|
||||
{'host': u'api',
|
||||
'style': u'rpc'},
|
||||
)
|
||||
token_revoke = bb.Route(
|
||||
'token/revoke',
|
||||
False,
|
||||
bv.Void(),
|
||||
bv.Void(),
|
||||
bv.Void(),
|
||||
{'host': u'api',
|
||||
'style': u'rpc'},
|
||||
)
|
||||
|
||||
ROUTES = {
|
||||
'token/from_oauth1': token_from_oauth1,
|
||||
'token/revoke': token_revoke,
|
||||
}
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
class InvalidPathRootError(object):
|
||||
"""
|
||||
:ivar path_root: The latest path root id for user's team if the user is
|
||||
still in a team.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_path_root_value',
|
||||
'_path_root_present',
|
||||
]
|
||||
|
||||
_has_required_fields = False
|
||||
|
||||
def __init__(self,
|
||||
path_root=None):
|
||||
self._path_root_value = None
|
||||
self._path_root_present = False
|
||||
if path_root is not None:
|
||||
self.path_root = path_root
|
||||
|
||||
@property
|
||||
def path_root(self):
|
||||
"""
|
||||
The latest path root id for user's team if the user is still in a team.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._path_root_present:
|
||||
return self._path_root_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@path_root.setter
|
||||
def path_root(self, val):
|
||||
if val is None:
|
||||
del self.path_root
|
||||
return
|
||||
val = self._path_root_validator.validate(val)
|
||||
self._path_root_value = val
|
||||
self._path_root_present = True
|
||||
|
||||
@path_root.deleter
|
||||
def path_root(self):
|
||||
self._path_root_value = None
|
||||
self._path_root_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'InvalidPathRootError(path_root={!r})'.format(
|
||||
self._path_root_value,
|
||||
)
|
||||
|
||||
InvalidPathRootError_validator = bv.Struct(InvalidPathRootError)
|
||||
|
||||
class PathRoot(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar home: Paths are relative to the authenticating user's home directory,
|
||||
whether or not that user belongs to a team.
|
||||
:ivar member_home: Paths are relative to the authenticating team member's
|
||||
home directory. (This results in ``PathRootError.invalid`` if the user
|
||||
does not belong to a team.).
|
||||
:ivar str team: Paths are relative to the given team directory. (This
|
||||
results in :field:`PathRootError.invalid` if the user is not a member of
|
||||
the team associated with that path root id.).
|
||||
:ivar user_home: Paths are relative to the user's home directory. (This
|
||||
results in ``PathRootError.invalid`` if the belongs to a team.).
|
||||
:ivar str namespace_id: Paths are relative to given namespace id (This
|
||||
results in :field:`PathRootError.no_permission` if you don't have access
|
||||
to this namespace.).
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
home = None
|
||||
# Attribute is overwritten below the class definition
|
||||
member_home = None
|
||||
# Attribute is overwritten below the class definition
|
||||
user_home = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
@classmethod
|
||||
def team(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``team`` tag with value
|
||||
``val``.
|
||||
|
||||
:param str val:
|
||||
:rtype: PathRoot
|
||||
"""
|
||||
return cls('team', val)
|
||||
|
||||
@classmethod
|
||||
def namespace_id(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``namespace_id`` tag with
|
||||
value ``val``.
|
||||
|
||||
:param str val:
|
||||
:rtype: PathRoot
|
||||
"""
|
||||
return cls('namespace_id', val)
|
||||
|
||||
def is_home(self):
|
||||
"""
|
||||
Check if the union tag is ``home``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'home'
|
||||
|
||||
def is_member_home(self):
|
||||
"""
|
||||
Check if the union tag is ``member_home``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'member_home'
|
||||
|
||||
def is_team(self):
|
||||
"""
|
||||
Check if the union tag is ``team``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'team'
|
||||
|
||||
def is_user_home(self):
|
||||
"""
|
||||
Check if the union tag is ``user_home``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'user_home'
|
||||
|
||||
def is_namespace_id(self):
|
||||
"""
|
||||
Check if the union tag is ``namespace_id``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'namespace_id'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def get_team(self):
|
||||
"""
|
||||
Paths are relative to the given team directory. (This results in
|
||||
``PathRootError.invalid`` if the user is not a member of the team
|
||||
associated with that path root id.).
|
||||
|
||||
Only call this if :meth:`is_team` is true.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.is_team():
|
||||
raise AttributeError("tag 'team' not set")
|
||||
return self._value
|
||||
|
||||
def get_namespace_id(self):
|
||||
"""
|
||||
Paths are relative to given namespace id (This results in
|
||||
``PathRootError.no_permission`` if you don't have access to this
|
||||
namespace.).
|
||||
|
||||
Only call this if :meth:`is_namespace_id` is true.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.is_namespace_id():
|
||||
raise AttributeError("tag 'namespace_id' not set")
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return 'PathRoot(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PathRoot_validator = bv.Union(PathRoot)
|
||||
|
||||
class PathRootError(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar InvalidPathRootError invalid: The path root id value in
|
||||
Dropbox-API-Path-Root header is no longer valid.
|
||||
:ivar no_permission: You don't have permission to access the path root id in
|
||||
Dropbox-API-Path-Root header.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
no_permission = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
@classmethod
|
||||
def invalid(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``invalid`` tag with value
|
||||
``val``.
|
||||
|
||||
:param InvalidPathRootError val:
|
||||
:rtype: PathRootError
|
||||
"""
|
||||
return cls('invalid', val)
|
||||
|
||||
def is_invalid(self):
|
||||
"""
|
||||
Check if the union tag is ``invalid``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'invalid'
|
||||
|
||||
def is_no_permission(self):
|
||||
"""
|
||||
Check if the union tag is ``no_permission``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'no_permission'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def get_invalid(self):
|
||||
"""
|
||||
The path root id value in Dropbox-API-Path-Root header is no longer
|
||||
valid.
|
||||
|
||||
Only call this if :meth:`is_invalid` is true.
|
||||
|
||||
:rtype: InvalidPathRootError
|
||||
"""
|
||||
if not self.is_invalid():
|
||||
raise AttributeError("tag 'invalid' not set")
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return 'PathRootError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PathRootError_validator = bv.Union(PathRootError)
|
||||
|
||||
Date_validator = bv.Timestamp(u'%Y-%m-%d')
|
||||
DisplayName_validator = bv.String(min_length=1, pattern=u'[^/:?*<>"|]*')
|
||||
DisplayNameLegacy_validator = bv.String(min_length=1)
|
||||
DropboxTimestamp_validator = bv.Timestamp(u'%Y-%m-%dT%H:%M:%SZ')
|
||||
EmailAddress_validator = bv.String(max_length=255, pattern=u"^['&A-Za-z0-9._%+-]+@[A-Za-z0-9-][A-Za-z0-9.-]*.[A-Za-z]{2,15}$")
|
||||
# A ISO639-1 code.
|
||||
LanguageCode_validator = bv.String(min_length=2)
|
||||
NamePart_validator = bv.String(min_length=1, max_length=100, pattern=u'[^/:?*<>"|]*')
|
||||
NamespaceId_validator = bv.String(pattern=u'[-_0-9a-zA-Z:]+')
|
||||
OptionalNamePart_validator = bv.String(max_length=100, pattern=u'[^/:?*<>"|]*')
|
||||
PathRootId_validator = NamespaceId_validator
|
||||
SessionId_validator = bv.String()
|
||||
SharedFolderId_validator = NamespaceId_validator
|
||||
InvalidPathRootError._path_root_validator = bv.Nullable(PathRootId_validator)
|
||||
InvalidPathRootError._all_field_names_ = set(['path_root'])
|
||||
InvalidPathRootError._all_fields_ = [('path_root', InvalidPathRootError._path_root_validator)]
|
||||
|
||||
PathRoot._home_validator = bv.Void()
|
||||
PathRoot._member_home_validator = bv.Void()
|
||||
PathRoot._team_validator = PathRootId_validator
|
||||
PathRoot._user_home_validator = bv.Void()
|
||||
PathRoot._namespace_id_validator = PathRootId_validator
|
||||
PathRoot._other_validator = bv.Void()
|
||||
PathRoot._tagmap = {
|
||||
'home': PathRoot._home_validator,
|
||||
'member_home': PathRoot._member_home_validator,
|
||||
'team': PathRoot._team_validator,
|
||||
'user_home': PathRoot._user_home_validator,
|
||||
'namespace_id': PathRoot._namespace_id_validator,
|
||||
'other': PathRoot._other_validator,
|
||||
}
|
||||
|
||||
PathRoot.home = PathRoot('home')
|
||||
PathRoot.member_home = PathRoot('member_home')
|
||||
PathRoot.user_home = PathRoot('user_home')
|
||||
PathRoot.other = PathRoot('other')
|
||||
|
||||
PathRootError._invalid_validator = InvalidPathRootError_validator
|
||||
PathRootError._no_permission_validator = bv.Void()
|
||||
PathRootError._other_validator = bv.Void()
|
||||
PathRootError._tagmap = {
|
||||
'invalid': PathRootError._invalid_validator,
|
||||
'no_permission': PathRootError._no_permission_validator,
|
||||
'other': PathRootError._other_validator,
|
||||
}
|
||||
|
||||
PathRootError.no_permission = PathRootError('no_permission')
|
||||
PathRootError.other = PathRootError('other')
|
||||
|
||||
ROUTES = {
|
||||
}
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
__all__ = [
|
||||
'Dropbox',
|
||||
'DropboxTeam',
|
||||
'create_session',
|
||||
]
|
||||
|
||||
# This should always be 0.0.0 in master. Only update this after tagging
|
||||
# before release.
|
||||
__version__ = '0.0.0'
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
from . import files, stone_serializers
|
||||
from .auth import (
|
||||
AuthError_validator,
|
||||
RateLimitError_validator,
|
||||
)
|
||||
from .base import DropboxBase
|
||||
from .base_team import DropboxTeamBase
|
||||
from .exceptions import (
|
||||
ApiError,
|
||||
AuthError,
|
||||
BadInputError,
|
||||
HttpError,
|
||||
InternalServerError,
|
||||
RateLimitError,
|
||||
)
|
||||
from .session import (
|
||||
API_HOST,
|
||||
API_CONTENT_HOST,
|
||||
API_NOTIFICATION_HOST,
|
||||
HOST_API,
|
||||
HOST_CONTENT,
|
||||
HOST_NOTIFY,
|
||||
pinned_session,
|
||||
)
|
||||
|
||||
class RouteResult(object):
|
||||
"""The successful result of a call to a route."""
|
||||
|
||||
def __init__(self, obj_result, http_resp=None):
|
||||
"""
|
||||
:param str obj_result: The result of a route not including the binary
|
||||
payload portion, if one exists. Must be serialized JSON.
|
||||
:param requests.models.Response http_resp: A raw HTTP response. It will
|
||||
be used to stream the binary-body payload of the response.
|
||||
"""
|
||||
assert isinstance(obj_result, six.string_types), \
|
||||
'obj_result: expected string, got %r' % type(obj_result)
|
||||
if http_resp is not None:
|
||||
assert isinstance(http_resp, requests.models.Response), \
|
||||
'http_resp: expected requests.models.Response, got %r' % \
|
||||
type(http_resp)
|
||||
self.obj_result = obj_result
|
||||
self.http_resp = http_resp
|
||||
|
||||
class RouteErrorResult(object):
|
||||
"""The error result of a call to a route."""
|
||||
|
||||
def __init__(self, request_id, obj_result):
|
||||
"""
|
||||
:param str request_id: A request_id can be shared with Dropbox Support
|
||||
to pinpoint the exact request that returns an error.
|
||||
:param str obj_result: The result of a route not including the binary
|
||||
payload portion, if one exists.
|
||||
"""
|
||||
self.request_id = request_id
|
||||
self.obj_result = obj_result
|
||||
|
||||
def create_session(max_connections=8, proxies=None):
|
||||
"""
|
||||
Creates a session object that can be used by multiple :class:`Dropbox` and
|
||||
:class:`DropboxTeam` instances. This lets you share a connection pool
|
||||
amongst them, as well as proxy parameters.
|
||||
|
||||
:param int max_connections: Maximum connection pool size.
|
||||
:param dict proxies: See the `requests module
|
||||
<http://docs.python-requests.org/en/latest/user/advanced/#proxies>`_
|
||||
for more details.
|
||||
:rtype: :class:`requests.sessions.Session`. `See the requests module
|
||||
<http://docs.python-requests.org/en/latest/user/advanced/#session-objects>`_
|
||||
for more details.
|
||||
"""
|
||||
# We only need as many pool_connections as we have unique hostnames.
|
||||
session = pinned_session(pool_maxsize=max_connections)
|
||||
if proxies:
|
||||
session.proxies = proxies
|
||||
return session
|
||||
|
||||
class _DropboxTransport(object):
|
||||
"""
|
||||
Responsible for implementing the wire protocol for making requests to the
|
||||
Dropbox API.
|
||||
"""
|
||||
|
||||
_API_VERSION = '2'
|
||||
|
||||
# Download style means that the route argument goes in a Dropbox-API-Arg
|
||||
# header, and the result comes back in a Dropbox-API-Result header. The
|
||||
# HTTP response body contains a binary payload.
|
||||
_ROUTE_STYLE_DOWNLOAD = 'download'
|
||||
|
||||
# Upload style means that the route argument goes in a Dropbox-API-Arg
|
||||
# header. The HTTP request body contains a binary payload. The result
|
||||
# comes back in a Dropbox-API-Result header.
|
||||
_ROUTE_STYLE_UPLOAD = 'upload'
|
||||
|
||||
# RPC style means that the argument and result of a route are contained in
|
||||
# the HTTP body.
|
||||
_ROUTE_STYLE_RPC = 'rpc'
|
||||
|
||||
# This is the default longest time we'll block on receiving data from the server
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
|
||||
def __init__(self,
|
||||
oauth2_access_token,
|
||||
max_retries_on_error=4,
|
||||
max_retries_on_rate_limit=None,
|
||||
user_agent=None,
|
||||
session=None,
|
||||
headers=None,
|
||||
timeout=_DEFAULT_TIMEOUT):
|
||||
"""
|
||||
:param str oauth2_access_token: OAuth2 access token for making client
|
||||
requests.
|
||||
|
||||
:param int max_retries_on_error: On 5xx errors, the number of times to
|
||||
retry.
|
||||
:param Optional[int] max_retries_on_rate_limit: On 429 errors, the
|
||||
number of times to retry. If `None`, always retries.
|
||||
:param str user_agent: The user agent to use when making requests. This
|
||||
helps us identify requests coming from your application. We
|
||||
recommend you use the format "AppName/Version". If set, we append
|
||||
"/OfficialDropboxPythonSDKv2/__version__" to the user_agent,
|
||||
:param session: If not provided, a new session (connection pool) is
|
||||
created. To share a session across multiple clients, use
|
||||
:func:`create_session`.
|
||||
:type session: :class:`requests.sessions.Session`
|
||||
:param dict headers: Additional headers to add to requests.
|
||||
:param Optional[float] timeout: Maximum duration in seconds that
|
||||
client will wait for any single packet from the
|
||||
server. After the timeout the client will give up on
|
||||
connection. If `None`, client will wait forever. Defaults
|
||||
to 30 seconds.
|
||||
"""
|
||||
assert len(oauth2_access_token) > 0, \
|
||||
'OAuth2 access token cannot be empty.'
|
||||
assert headers is None or isinstance(headers, dict), \
|
||||
'Expected dict, got %r' % headers
|
||||
self._oauth2_access_token = oauth2_access_token
|
||||
|
||||
self._max_retries_on_error = max_retries_on_error
|
||||
self._max_retries_on_rate_limit = max_retries_on_rate_limit
|
||||
if session:
|
||||
assert isinstance(session, requests.sessions.Session), \
|
||||
'Expected requests.sessions.Session, got %r' % session
|
||||
self._session = session
|
||||
else:
|
||||
self._session = create_session()
|
||||
self._headers = headers
|
||||
|
||||
base_user_agent = 'OfficialDropboxPythonSDKv2/' + __version__
|
||||
if user_agent:
|
||||
self._raw_user_agent = user_agent
|
||||
self._user_agent = '{}/{}'.format(user_agent, base_user_agent)
|
||||
else:
|
||||
self._raw_user_agent = None
|
||||
self._user_agent = base_user_agent
|
||||
|
||||
self._logger = logging.getLogger('dropbox')
|
||||
|
||||
self._host_map = {HOST_API: API_HOST,
|
||||
HOST_CONTENT: API_CONTENT_HOST,
|
||||
HOST_NOTIFY: API_NOTIFICATION_HOST}
|
||||
|
||||
self._timeout = timeout
|
||||
|
||||
def request(self,
|
||||
route,
|
||||
namespace,
|
||||
request_arg,
|
||||
request_binary,
|
||||
timeout=None):
|
||||
"""
|
||||
Makes a request to the Dropbox API and in the process validates that
|
||||
the route argument and result are the expected data types. The
|
||||
request_arg is converted to JSON based on the arg_data_type. Likewise,
|
||||
the response is deserialized from JSON and converted to an object based
|
||||
on the {result,error}_data_type.
|
||||
|
||||
:param host: The Dropbox API host to connect to.
|
||||
:param route: The route to make the request to.
|
||||
:type route: :class:`.datatypes.stone_base.Route`
|
||||
:param request_arg: Argument for the route that conforms to the
|
||||
validator specified by route.arg_type.
|
||||
:param request_binary: String or file pointer representing the binary
|
||||
payload. Use None if there is no binary payload.
|
||||
:param Optional[float] timeout: Maximum duration in seconds
|
||||
that client will wait for any single packet from the
|
||||
server. After the timeout the client will give up on
|
||||
connection. If `None`, will use default timeout set on
|
||||
Dropbox object. Defaults to `None`.
|
||||
:return: The route's result.
|
||||
"""
|
||||
host = route.attrs['host'] or 'api'
|
||||
route_name = namespace + '/' + route.name
|
||||
route_style = route.attrs['style'] or 'rpc'
|
||||
serialized_arg = stone_serializers.json_encode(route.arg_type,
|
||||
request_arg)
|
||||
|
||||
if (timeout is None and
|
||||
route == files.list_folder_longpoll):
|
||||
# The client normally sends a timeout value to the
|
||||
# longpoll route. The server will respond after
|
||||
# <timeout> + random(0, 90) seconds. We increase the
|
||||
# socket timeout to the longpoll timeout value plus 90
|
||||
# seconds so that we don't cut the server response short
|
||||
# due to a shorter socket timeout.
|
||||
# NB: This is done here because base.py is auto-generated
|
||||
timeout = request_arg.timeout + 90
|
||||
|
||||
res = self.request_json_string_with_retry(host,
|
||||
route_name,
|
||||
route_style,
|
||||
serialized_arg,
|
||||
request_binary,
|
||||
timeout=timeout)
|
||||
decoded_obj_result = json.loads(res.obj_result)
|
||||
if isinstance(res, RouteResult):
|
||||
returned_data_type = route.result_type
|
||||
obj = decoded_obj_result
|
||||
elif isinstance(res, RouteErrorResult):
|
||||
returned_data_type = route.error_type
|
||||
obj = decoded_obj_result['error']
|
||||
user_message = decoded_obj_result.get('user_message')
|
||||
user_message_text = user_message and user_message.get('text')
|
||||
user_message_locale = user_message and user_message.get('locale')
|
||||
else:
|
||||
raise AssertionError('Expected RouteResult or RouteErrorResult, '
|
||||
'but res is %s' % type(res))
|
||||
|
||||
deserialized_result = stone_serializers.json_compat_obj_decode(
|
||||
returned_data_type, obj, strict=False)
|
||||
|
||||
if isinstance(res, RouteErrorResult):
|
||||
raise ApiError(res.request_id,
|
||||
deserialized_result,
|
||||
user_message_text,
|
||||
user_message_locale)
|
||||
elif route_style == self._ROUTE_STYLE_DOWNLOAD:
|
||||
return (deserialized_result, res.http_resp)
|
||||
else:
|
||||
return deserialized_result
|
||||
|
||||
def request_json_object(self,
|
||||
host,
|
||||
route_name,
|
||||
route_style,
|
||||
request_arg,
|
||||
request_binary,
|
||||
timeout=None):
|
||||
"""
|
||||
Makes a request to the Dropbox API, taking a JSON-serializable Python
|
||||
object as an argument, and returning one as a response.
|
||||
|
||||
:param host: The Dropbox API host to connect to.
|
||||
:param route_name: The name of the route to invoke.
|
||||
:param route_style: The style of the route.
|
||||
:param str request_arg: A JSON-serializable Python object representing
|
||||
the argument for the route.
|
||||
:param Optional[bytes] request_binary: Bytes representing the binary
|
||||
payload. Use None if there is no binary payload.
|
||||
:param Optional[float] timeout: Maximum duration in seconds
|
||||
that client will wait for any single packet from the
|
||||
server. After the timeout the client will give up on
|
||||
connection. If `None`, will use default timeout set on
|
||||
Dropbox object. Defaults to `None`.
|
||||
:return: The route's result as a JSON-serializable Python object.
|
||||
"""
|
||||
serialized_arg = json.dumps(request_arg)
|
||||
res = self.request_json_string_with_retry(host,
|
||||
route_name,
|
||||
route_style,
|
||||
serialized_arg,
|
||||
request_binary,
|
||||
timeout=timeout)
|
||||
# This can throw a ValueError if the result is not deserializable,
|
||||
# but that would be completely unexpected.
|
||||
deserialized_result = json.loads(res.obj_result)
|
||||
if isinstance(res, RouteResult) and res.http_resp is not None:
|
||||
return (deserialized_result, res.http_resp)
|
||||
else:
|
||||
return deserialized_result
|
||||
|
||||
def request_json_string_with_retry(self,
|
||||
host,
|
||||
route_name,
|
||||
route_style,
|
||||
request_json_arg,
|
||||
request_binary,
|
||||
timeout=None):
|
||||
"""
|
||||
See :meth:`request_json_object` for description of parameters.
|
||||
|
||||
:param request_json_arg: A string representing the serialized JSON
|
||||
argument to the route.
|
||||
"""
|
||||
attempt = 0
|
||||
rate_limit_errors = 0
|
||||
while True:
|
||||
self._logger.info('Request to %s', route_name)
|
||||
try:
|
||||
return self.request_json_string(host,
|
||||
route_name,
|
||||
route_style,
|
||||
request_json_arg,
|
||||
request_binary,
|
||||
timeout=timeout)
|
||||
except InternalServerError as e:
|
||||
attempt += 1
|
||||
if attempt <= self._max_retries_on_error:
|
||||
# Use exponential backoff
|
||||
backoff = 2**attempt * random.random()
|
||||
self._logger.info(
|
||||
'HttpError status_code=%s: Retrying in %.1f seconds',
|
||||
e.status_code, backoff)
|
||||
time.sleep(backoff)
|
||||
else:
|
||||
raise
|
||||
except RateLimitError as e:
|
||||
rate_limit_errors += 1
|
||||
if (self._max_retries_on_rate_limit is None or
|
||||
self._max_retries_on_rate_limit >= rate_limit_errors):
|
||||
# Set default backoff to 5 seconds.
|
||||
backoff = e.backoff if e.backoff is not None else 5.0
|
||||
self._logger.info(
|
||||
'Ratelimit: Retrying in %.1f seconds.', backoff)
|
||||
time.sleep(backoff)
|
||||
else:
|
||||
raise
|
||||
|
||||
def request_json_string(self,
|
||||
host,
|
||||
func_name,
|
||||
route_style,
|
||||
request_json_arg,
|
||||
request_binary,
|
||||
timeout=None):
|
||||
"""
|
||||
See :meth:`request_json_string_with_retry` for description of
|
||||
parameters.
|
||||
"""
|
||||
if host not in self._host_map:
|
||||
raise ValueError('Unknown value for host: %r' % host)
|
||||
|
||||
if not isinstance(request_binary, (six.binary_type, type(None))):
|
||||
# Disallow streams and file-like objects even though the underlying
|
||||
# requests library supports them. This is to prevent incorrect
|
||||
# behavior when a non-rewindable stream is read from, but the
|
||||
# request fails and needs to be re-tried at a later time.
|
||||
raise TypeError('expected request_binary as binary type, got %s' %
|
||||
type(request_binary))
|
||||
|
||||
# Fully qualified hostname
|
||||
fq_hostname = self._host_map[host]
|
||||
url = self._get_route_url(fq_hostname, func_name)
|
||||
|
||||
headers = {'User-Agent': self._user_agent}
|
||||
if host != HOST_NOTIFY:
|
||||
headers['Authorization'] = 'Bearer %s' % self._oauth2_access_token
|
||||
if self._headers:
|
||||
headers.update(self._headers)
|
||||
|
||||
# The contents of the body of the HTTP request
|
||||
body = None
|
||||
# Whether the response should be streamed incrementally, or buffered
|
||||
# entirely. If stream is True, the caller is responsible for closing
|
||||
# the HTTP response.
|
||||
stream = False
|
||||
|
||||
if route_style == self._ROUTE_STYLE_RPC:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
body = request_json_arg
|
||||
elif route_style == self._ROUTE_STYLE_DOWNLOAD:
|
||||
headers['Dropbox-API-Arg'] = request_json_arg
|
||||
stream = True
|
||||
elif route_style == self._ROUTE_STYLE_UPLOAD:
|
||||
headers['Content-Type'] = 'application/octet-stream'
|
||||
headers['Dropbox-API-Arg'] = request_json_arg
|
||||
body = request_binary
|
||||
else:
|
||||
raise ValueError('Unknown operation style: %r' % route_style)
|
||||
|
||||
if timeout is None:
|
||||
timeout = self._timeout
|
||||
|
||||
r = self._session.post(url,
|
||||
headers=headers,
|
||||
data=body,
|
||||
stream=stream,
|
||||
verify=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
request_id = r.headers.get('x-dropbox-request-id')
|
||||
if r.status_code >= 500:
|
||||
raise InternalServerError(request_id, r.status_code, r.text)
|
||||
elif r.status_code == 400:
|
||||
raise BadInputError(request_id, r.text)
|
||||
elif r.status_code == 401:
|
||||
assert r.headers.get('content-type') == 'application/json', (
|
||||
'Expected content-type to be application/json, got %r' %
|
||||
r.headers.get('content-type'))
|
||||
err = stone_serializers.json_compat_obj_decode(
|
||||
AuthError_validator, r.json()['error'])
|
||||
raise AuthError(request_id, err)
|
||||
elif r.status_code == 429:
|
||||
err = None
|
||||
if r.headers.get('content-type') == 'application/json':
|
||||
err = stone_serializers.json_compat_obj_decode(
|
||||
RateLimitError_validator, r.json()['error'])
|
||||
retry_after = err.retry_after
|
||||
else:
|
||||
retry_after_str = r.headers.get('retry-after')
|
||||
if retry_after_str is not None:
|
||||
retry_after = int(retry_after_str)
|
||||
else:
|
||||
retry_after = None
|
||||
raise RateLimitError(request_id, err, retry_after)
|
||||
elif 200 <= r.status_code <= 299:
|
||||
if route_style == self._ROUTE_STYLE_DOWNLOAD:
|
||||
raw_resp = r.headers['dropbox-api-result']
|
||||
else:
|
||||
assert r.headers.get('content-type') == 'application/json', (
|
||||
'Expected content-type to be application/json, got %r' %
|
||||
r.headers.get('content-type'))
|
||||
raw_resp = r.content.decode('utf-8')
|
||||
if route_style == self._ROUTE_STYLE_DOWNLOAD:
|
||||
return RouteResult(raw_resp, r)
|
||||
else:
|
||||
return RouteResult(raw_resp)
|
||||
elif r.status_code in (403, 404, 409):
|
||||
raw_resp = r.content.decode('utf-8')
|
||||
return RouteErrorResult(request_id, raw_resp)
|
||||
else:
|
||||
raise HttpError(request_id, r.status_code, r.text)
|
||||
|
||||
def _get_route_url(self, hostname, route_name):
|
||||
"""Returns the URL of the route.
|
||||
|
||||
:param str hostname: Hostname to make the request to.
|
||||
:param str route_name: Name of the route.
|
||||
:rtype: str
|
||||
"""
|
||||
return 'https://{hostname}/{version}/{route_name}'.format(
|
||||
hostname=hostname,
|
||||
version=Dropbox._API_VERSION,
|
||||
route_name=route_name,
|
||||
)
|
||||
|
||||
def _save_body_to_file(self, download_path, http_resp, chunksize=2**16):
|
||||
"""
|
||||
Saves the body of an HTTP response to a file.
|
||||
|
||||
:param str download_path: Local path to save data to.
|
||||
:param http_resp: The HTTP response whose body will be saved.
|
||||
:type http_resp: :class:`requests.models.Response`
|
||||
:rtype: None
|
||||
"""
|
||||
with open(download_path, 'wb') as f:
|
||||
with contextlib.closing(http_resp):
|
||||
for c in http_resp.iter_content(chunksize):
|
||||
f.write(c)
|
||||
|
||||
class Dropbox(_DropboxTransport, DropboxBase):
|
||||
"""
|
||||
Use this class to make requests to the Dropbox API using a user's access
|
||||
token. Methods of this class are meant to act on the corresponding user's
|
||||
Dropbox.
|
||||
"""
|
||||
pass
|
||||
|
||||
class DropboxTeam(_DropboxTransport, DropboxTeamBase):
|
||||
"""
|
||||
Use this class to make requests to the Dropbox API using a team's access
|
||||
token. Methods of this class are meant to act on the team, but there is
|
||||
also an :meth:`as_user` method for assuming a team member's identity.
|
||||
"""
|
||||
|
||||
def as_user(self, team_member_id):
|
||||
"""
|
||||
Allows a team credential to assume the identity of a member of the
|
||||
team.
|
||||
|
||||
:return: A :class:`Dropbox` object that can be used to query on behalf
|
||||
of this member of the team.
|
||||
:rtype: Dropbox
|
||||
"""
|
||||
new_headers = self._headers.copy() if self._headers else {}
|
||||
new_headers['Dropbox-API-Select-User'] = team_member_id
|
||||
return Dropbox(
|
||||
self._oauth2_access_token,
|
||||
max_retries_on_error=self._max_retries_on_error,
|
||||
max_retries_on_rate_limit=self._max_retries_on_rate_limit,
|
||||
user_agent=self._raw_user_agent,
|
||||
session=self._session,
|
||||
headers=new_headers,
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
class DropboxException(Exception):
|
||||
"""All errors related to making an API request extend this."""
|
||||
|
||||
def __init__(self, request_id, *args, **kwargs):
|
||||
# A request_id can be shared with Dropbox Support to pinpoint the exact
|
||||
# request that returns an error.
|
||||
super(DropboxException, self).__init__(request_id, *args, **kwargs)
|
||||
self.request_id = request_id
|
||||
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
|
||||
|
||||
class ApiError(DropboxException):
|
||||
"""Errors produced by the Dropbox API."""
|
||||
|
||||
def __init__(self, request_id, error, user_message_text, user_message_locale):
|
||||
"""
|
||||
:param (str) request_id: A request_id can be shared with Dropbox
|
||||
Support to pinpoint the exact request that returns an error.
|
||||
:param error: An instance of the error data type for the route.
|
||||
:param (str) user_message_text: A human-readable message that can be
|
||||
displayed to the end user. Is None, if unavailable.
|
||||
:param (str) user_message_locale: The locale of ``user_message_text``,
|
||||
if present.
|
||||
"""
|
||||
super(ApiError, self).__init__(request_id, error)
|
||||
self.error = error
|
||||
self.user_message_text = user_message_text
|
||||
self.user_message_locale = user_message_locale
|
||||
|
||||
def __repr__(self):
|
||||
return 'ApiError({!r}, {})'.format(self.request_id, self.error)
|
||||
|
||||
|
||||
class HttpError(DropboxException):
|
||||
"""Errors produced at the HTTP layer."""
|
||||
|
||||
def __init__(self, request_id, status_code, body):
|
||||
super(HttpError, self).__init__(request_id, status_code, body)
|
||||
self.status_code = status_code
|
||||
self.body = body
|
||||
|
||||
def __repr__(self):
|
||||
return 'HttpError({!r}, {}, {!r})'.format(self.request_id,
|
||||
self.status_code, self.body)
|
||||
|
||||
|
||||
class BadInputError(HttpError):
|
||||
"""Errors due to bad input parameters to an API Operation."""
|
||||
|
||||
def __init__(self, request_id, message):
|
||||
super(BadInputError, self).__init__(request_id, 400, message)
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return 'BadInputError({!r}, {!r})'.format(self.request_id, self.message)
|
||||
|
||||
|
||||
class AuthError(HttpError):
|
||||
"""Errors due to invalid authentication credentials."""
|
||||
|
||||
def __init__(self, request_id, error):
|
||||
super(AuthError, self).__init__(request_id, 401, None)
|
||||
self.error = error
|
||||
|
||||
def __repr__(self):
|
||||
return 'AuthError({!r}, {!r})'.format(self.request_id, self.error)
|
||||
|
||||
|
||||
class RateLimitError(HttpError):
|
||||
"""Error caused by rate limiting."""
|
||||
|
||||
def __init__(self, request_id, error=None, backoff=None):
|
||||
super(RateLimitError, self).__init__(request_id, 429, None)
|
||||
self.error = error
|
||||
self.backoff = backoff
|
||||
|
||||
def __repr__(self):
|
||||
return 'RateLimitError({!r}, {!r}, {!r})'.format(
|
||||
self.request_id, self.error, self.backoff)
|
||||
|
||||
|
||||
class InternalServerError(HttpError):
|
||||
"""Errors due to a problem on Dropbox."""
|
||||
|
||||
def __repr__(self):
|
||||
return 'InternalServerError({!r}, {}, {!r})'.format(
|
||||
self.request_id, self.status_code, self.body)
|
||||
@@ -1,515 +0,0 @@
|
||||
__all__ = [
|
||||
'BadRequestException',
|
||||
'BadStateException',
|
||||
'CsrfException',
|
||||
'DropboxOAuth2Flow',
|
||||
'DropboxOAuth2FlowNoRedirect',
|
||||
'NotApprovedException',
|
||||
'OAuth2FlowNoRedirectResult',
|
||||
'OAuth2FlowResult',
|
||||
'ProviderException',
|
||||
]
|
||||
|
||||
import base64
|
||||
import os
|
||||
import six
|
||||
import urllib
|
||||
|
||||
from .session import (
|
||||
API_HOST,
|
||||
WEB_HOST,
|
||||
pinned_session,
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
url_path_quote = urllib.parse.quote # pylint: disable=no-member,useless-suppression
|
||||
url_encode = urllib.parse.urlencode # pylint: disable=no-member,useless-suppression
|
||||
else:
|
||||
url_path_quote = urllib.quote # pylint: disable=no-member,useless-suppression
|
||||
url_encode = urllib.urlencode # pylint: disable=no-member,useless-suppression
|
||||
|
||||
|
||||
class OAuth2FlowNoRedirectResult(object):
|
||||
"""
|
||||
Authorization information for an OAuth2Flow performed with no redirect.
|
||||
"""
|
||||
|
||||
def __init__(self, access_token, account_id, user_id):
|
||||
"""
|
||||
Args:
|
||||
access_token (str): Token to be used to authenticate later
|
||||
requests.
|
||||
account_id (str): The Dropbox user's account ID.
|
||||
user_id (str): Deprecated (use account_id instead).
|
||||
"""
|
||||
self.access_token = access_token
|
||||
self.account_id = account_id
|
||||
self.user_id = user_id
|
||||
|
||||
def __repr__(self):
|
||||
return 'OAuth2FlowNoRedirectResult(%r, %r, %r)' % (
|
||||
self.access_token,
|
||||
self.account_id,
|
||||
self.user_id,
|
||||
)
|
||||
|
||||
|
||||
class OAuth2FlowResult(OAuth2FlowNoRedirectResult):
|
||||
"""
|
||||
Authorization information for an OAuth2Flow with redirect.
|
||||
"""
|
||||
|
||||
def __init__(self, access_token, account_id, user_id, url_state):
|
||||
"""
|
||||
Same as OAuth2FlowNoRedirectResult but with url_state.
|
||||
|
||||
Args:
|
||||
url_state (str): The url state that was set by
|
||||
:meth:`DropboxOAuth2Flow.start`.
|
||||
"""
|
||||
super(OAuth2FlowResult, self).__init__(
|
||||
access_token, account_id, user_id)
|
||||
self.url_state = url_state
|
||||
|
||||
@classmethod
|
||||
def from_no_redirect_result(cls, result, url_state):
|
||||
assert isinstance(result, OAuth2FlowNoRedirectResult)
|
||||
return cls(
|
||||
result.access_token, result.account_id, result.user_id, url_state)
|
||||
|
||||
def __repr__(self):
|
||||
return 'OAuth2FlowResult(%r, %r, %r, %r)' % (
|
||||
self.access_token,
|
||||
self.account_id,
|
||||
self.user_id,
|
||||
self.url_state,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2FlowBase(object):
|
||||
|
||||
def __init__(self, consumer_key, consumer_secret, locale=None):
|
||||
self.consumer_key = consumer_key
|
||||
self.consumer_secret = consumer_secret
|
||||
self.locale = locale
|
||||
self.requests_session = pinned_session()
|
||||
|
||||
def _get_authorize_url(self, redirect_uri, state):
|
||||
params = dict(response_type='code',
|
||||
client_id=self.consumer_key)
|
||||
if redirect_uri is not None:
|
||||
params['redirect_uri'] = redirect_uri
|
||||
if state is not None:
|
||||
params['state'] = state
|
||||
|
||||
return self.build_url('/oauth2/authorize', params, WEB_HOST)
|
||||
|
||||
def _finish(self, code, redirect_uri):
|
||||
url = self.build_url('/oauth2/token')
|
||||
params = {'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'client_id': self.consumer_key,
|
||||
'client_secret': self.consumer_secret,
|
||||
}
|
||||
if self.locale is not None:
|
||||
params['locale'] = self.locale
|
||||
if redirect_uri is not None:
|
||||
params['redirect_uri'] = redirect_uri
|
||||
|
||||
resp = self.requests_session.post(url, data=params)
|
||||
resp.raise_for_status()
|
||||
|
||||
d = resp.json()
|
||||
|
||||
if 'team_id' in d:
|
||||
account_id = d['team_id']
|
||||
else:
|
||||
account_id = d['account_id']
|
||||
|
||||
access_token = d['access_token']
|
||||
uid = d['uid']
|
||||
|
||||
return OAuth2FlowNoRedirectResult(
|
||||
access_token,
|
||||
account_id,
|
||||
uid)
|
||||
|
||||
def build_path(self, target, params=None):
|
||||
"""Build the path component for an API URL.
|
||||
|
||||
This method urlencodes the parameters, adds them
|
||||
to the end of the target url, and puts a marker for the API
|
||||
version in front.
|
||||
|
||||
:param str target: A target url (e.g. '/files') to build upon.
|
||||
:param dict params: Optional dictionary of parameters (name to value).
|
||||
:return: The path and parameters components of an API URL.
|
||||
:rtype: str
|
||||
"""
|
||||
if six.PY2 and isinstance(target, six.text_type):
|
||||
target = target.encode('utf8')
|
||||
|
||||
target_path = url_path_quote(target)
|
||||
|
||||
params = params or {}
|
||||
params = params.copy()
|
||||
|
||||
if self.locale:
|
||||
params['locale'] = self.locale
|
||||
|
||||
if params:
|
||||
query_string = _params_to_urlencoded(params)
|
||||
return "%s?%s" % (target_path, query_string)
|
||||
else:
|
||||
return target_path
|
||||
|
||||
def build_url(self, target, params=None, host=API_HOST):
|
||||
"""Build an API URL.
|
||||
|
||||
This method adds scheme and hostname to the path
|
||||
returned from build_path.
|
||||
|
||||
:param str target: A target url (e.g. '/files') to build upon.
|
||||
:param dict params: Optional dictionary of parameters (name to value).
|
||||
:return: The full API URL.
|
||||
:rtype: str
|
||||
"""
|
||||
return "https://%s%s" % (host, self.build_path(target, params))
|
||||
|
||||
|
||||
class DropboxOAuth2FlowNoRedirect(DropboxOAuth2FlowBase):
|
||||
"""
|
||||
OAuth 2 authorization helper for apps that can't provide a redirect URI
|
||||
(such as the command-line example apps).
|
||||
|
||||
Example::
|
||||
|
||||
from dropbox import DropboxOAuth2FlowNoRedirect
|
||||
|
||||
auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, APP_SECRET)
|
||||
|
||||
authorize_url = auth_flow.start()
|
||||
print "1. Go to: " + authorize_url
|
||||
print "2. Click \\"Allow\\" (you might have to log in first)."
|
||||
print "3. Copy the authorization code."
|
||||
auth_code = raw_input("Enter the authorization code here: ").strip()
|
||||
|
||||
try:
|
||||
oauth_result = auth_flow.finish(auth_code)
|
||||
except Exception, e:
|
||||
print('Error: %s' % (e,))
|
||||
return
|
||||
|
||||
dbx = Dropbox(oauth_result.access_token)
|
||||
"""
|
||||
|
||||
def __init__(self, consumer_key, consumer_secret, locale=None): # noqa: E501; pylint: disable=useless-super-delegation
|
||||
"""
|
||||
Construct an instance.
|
||||
|
||||
Parameters
|
||||
:param str consumer_key: Your API app's "app key".
|
||||
:param str consumer_secret: Your API app's "app secret".
|
||||
:param str locale: The locale of the user of your application. For
|
||||
example "en" or "en_US". Some API calls return localized data and
|
||||
error messages; this setting tells the server which locale to use.
|
||||
By default, the server uses "en_US".
|
||||
"""
|
||||
# pylint: disable=useless-super-delegation
|
||||
super(DropboxOAuth2FlowNoRedirect, self).__init__(
|
||||
consumer_key,
|
||||
consumer_secret,
|
||||
locale,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the OAuth 2 authorization process.
|
||||
|
||||
:return: The URL for a page on Dropbox's website. This page will let
|
||||
the user "approve" your app, which gives your app permission to
|
||||
access the user's Dropbox account. Tell the user to visit this URL
|
||||
and approve your app.
|
||||
"""
|
||||
return self._get_authorize_url(None, None)
|
||||
|
||||
def finish(self, code):
|
||||
"""
|
||||
If the user approves your app, they will be presented with an
|
||||
"authorization code". Have the user copy/paste that authorization code
|
||||
into your app and then call this method to get an access token.
|
||||
|
||||
:param str code: The authorization code shown to the user when they
|
||||
approved your app.
|
||||
:rtype: OAuth2FlowNoRedirectResult
|
||||
:raises: The same exceptions as :meth:`DropboxOAuth2Flow.finish()`.
|
||||
"""
|
||||
return self._finish(code, None)
|
||||
|
||||
|
||||
class DropboxOAuth2Flow(DropboxOAuth2FlowBase):
|
||||
"""
|
||||
OAuth 2 authorization helper. Use this for web apps.
|
||||
|
||||
OAuth 2 has a two-step authorization process. The first step is having the
|
||||
user authorize your app. The second involves getting an OAuth 2 access
|
||||
token from Dropbox.
|
||||
|
||||
Example::
|
||||
|
||||
from dropbox import DropboxOAuth2Flow
|
||||
|
||||
def get_dropbox_auth_flow(web_app_session):
|
||||
redirect_uri = "https://my-web-server.org/dropbox-auth-finish"
|
||||
return DropboxOAuth2Flow(
|
||||
APP_KEY, APP_SECRET, redirect_uri, web_app_session,
|
||||
"dropbox-auth-csrf-token")
|
||||
|
||||
# URL handler for /dropbox-auth-start
|
||||
def dropbox_auth_start(web_app_session, request):
|
||||
authorize_url = get_dropbox_auth_flow(web_app_session).start()
|
||||
redirect_to(authorize_url)
|
||||
|
||||
# URL handler for /dropbox-auth-finish
|
||||
def dropbox_auth_finish(web_app_session, request):
|
||||
try:
|
||||
oauth_result = \\
|
||||
get_dropbox_auth_flow(web_app_session).finish(
|
||||
request.query_params)
|
||||
except BadRequestException, e:
|
||||
http_status(400)
|
||||
except BadStateException, e:
|
||||
# Start the auth flow again.
|
||||
redirect_to("/dropbox-auth-start")
|
||||
except CsrfException, e:
|
||||
http_status(403)
|
||||
except NotApprovedException, e:
|
||||
flash('Not approved? Why not?')
|
||||
return redirect_to("/home")
|
||||
except ProviderException, e:
|
||||
logger.log("Auth error: %s" % (e,))
|
||||
http_status(403)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, consumer_key, consumer_secret, redirect_uri, session,
|
||||
csrf_token_session_key, locale=None):
|
||||
"""
|
||||
Construct an instance.
|
||||
|
||||
:param str consumer_key: Your API app's "app key".
|
||||
:param str consumer_secret: Your API app's "app secret".
|
||||
:param str redirect_uri: The URI that the Dropbox server will redirect
|
||||
the user to after the user finishes authorizing your app. This URI
|
||||
must be HTTPS-based and pre-registered with the Dropbox servers,
|
||||
though localhost URIs are allowed without pre-registration and can
|
||||
be either HTTP or HTTPS.
|
||||
:param dict session: A dict-like object that represents the current
|
||||
user's web session (will be used to save the CSRF token).
|
||||
:param str csrf_token_session_key: The key to use when storing the CSRF
|
||||
token in the session (for example: "dropbox-auth-csrf-token").
|
||||
:param str locale: The locale of the user of your application. For
|
||||
example "en" or "en_US". Some API calls return localized data and
|
||||
error messages; this setting tells the server which locale to use.
|
||||
By default, the server uses "en_US".
|
||||
"""
|
||||
super(DropboxOAuth2Flow, self).__init__(consumer_key, consumer_secret, locale)
|
||||
self.redirect_uri = redirect_uri
|
||||
self.session = session
|
||||
self.csrf_token_session_key = csrf_token_session_key
|
||||
|
||||
def start(self, url_state=None):
|
||||
"""
|
||||
Starts the OAuth 2 authorization process.
|
||||
|
||||
This function builds an "authorization URL". You should redirect your
|
||||
user's browser to this URL, which will give them an opportunity to
|
||||
grant your app access to their Dropbox account. When the user
|
||||
completes this process, they will be automatically redirected to the
|
||||
``redirect_uri`` you passed in to the constructor.
|
||||
|
||||
This function will also save a CSRF token to
|
||||
``session[csrf_token_session_key]`` (as provided to the constructor).
|
||||
This CSRF token will be checked on :meth:`finish()` to prevent request
|
||||
forgery.
|
||||
|
||||
:param str url_state: Any data that you would like to keep in the URL
|
||||
through the authorization process. This exact value will be
|
||||
returned to you by :meth:`finish()`.
|
||||
:return: The URL for a page on Dropbox's website. This page will let
|
||||
the user "approve" your app, which gives your app permission to
|
||||
access the user's Dropbox account. Tell the user to visit this URL
|
||||
and approve your app.
|
||||
"""
|
||||
csrf_token = base64.urlsafe_b64encode(os.urandom(16)).decode('ascii')
|
||||
state = csrf_token
|
||||
if url_state is not None:
|
||||
state += "|" + url_state
|
||||
self.session[self.csrf_token_session_key] = csrf_token
|
||||
|
||||
return self._get_authorize_url(self.redirect_uri, state)
|
||||
|
||||
def finish(self, query_params):
|
||||
"""
|
||||
Call this after the user has visited the authorize URL (see
|
||||
:meth:`start()`), approved your app and was redirected to your redirect
|
||||
URI.
|
||||
|
||||
:param dict query_params: The query parameters on the GET request to
|
||||
your redirect URI.
|
||||
:rtype: OAuth2FlowResult
|
||||
:raises: :class:`BadRequestException` If the redirect URL was missing
|
||||
parameters or if the given parameters were not valid.
|
||||
:raises: :class:`BadStateException` If there's no CSRF token in the
|
||||
session.
|
||||
:raises: :class:`CsrfException` If the ``state`` query parameter
|
||||
doesn't contain the CSRF token from the user's session.
|
||||
:raises: :class:`NotApprovedException` If the user chose not to
|
||||
approve your app.
|
||||
:raises: :class:`ProviderException` If Dropbox redirected to your
|
||||
redirect URI with some unexpected error identifier and error message.
|
||||
"""
|
||||
# Check well-formedness of request.
|
||||
|
||||
state = query_params.get('state')
|
||||
if state is None:
|
||||
raise BadRequestException("Missing query parameter 'state'.")
|
||||
|
||||
error = query_params.get('error')
|
||||
error_description = query_params.get('error_description')
|
||||
code = query_params.get('code')
|
||||
|
||||
if error is not None and code is not None:
|
||||
raise BadRequestException(
|
||||
"Query parameters 'code' and 'error' are both set; "
|
||||
"only one must be set.")
|
||||
if error is None and code is None:
|
||||
raise BadRequestException(
|
||||
"Neither query parameter 'code' or 'error' is set.")
|
||||
|
||||
# Check CSRF token
|
||||
|
||||
if self.csrf_token_session_key not in self.session:
|
||||
raise BadStateException('Missing CSRF token in session.')
|
||||
csrf_token_from_session = self.session[self.csrf_token_session_key]
|
||||
if len(csrf_token_from_session) <= 20:
|
||||
raise AssertionError('CSRF token unexpectedly short: %r' %
|
||||
csrf_token_from_session)
|
||||
|
||||
split_pos = state.find('|')
|
||||
if split_pos < 0:
|
||||
given_csrf_token = state
|
||||
url_state = None
|
||||
else:
|
||||
given_csrf_token = state[0:split_pos]
|
||||
url_state = state[split_pos + 1:]
|
||||
|
||||
if not _safe_equals(csrf_token_from_session, given_csrf_token):
|
||||
raise CsrfException('expected %r, got %r' %
|
||||
(csrf_token_from_session, given_csrf_token))
|
||||
|
||||
del self.session[self.csrf_token_session_key]
|
||||
|
||||
# Check for error identifier
|
||||
|
||||
if error is not None:
|
||||
if error == 'access_denied':
|
||||
# The user clicked "Deny"
|
||||
if error_description is None:
|
||||
raise NotApprovedException(
|
||||
'No additional description from Dropbox')
|
||||
else:
|
||||
raise NotApprovedException(
|
||||
'Additional description from Dropbox: %s' %
|
||||
error_description)
|
||||
else:
|
||||
# All other errors
|
||||
full_message = error
|
||||
if error_description is not None:
|
||||
full_message += ": " + error_description
|
||||
raise ProviderException(full_message)
|
||||
|
||||
# If everything went ok, make the network call to get an access token.
|
||||
|
||||
no_redirect_result = self._finish(code, self.redirect_uri)
|
||||
return OAuth2FlowResult.from_no_redirect_result(
|
||||
no_redirect_result, url_state)
|
||||
|
||||
|
||||
class BadRequestException(Exception):
|
||||
"""
|
||||
Thrown if the redirect URL was missing parameters or if the
|
||||
given parameters were not valid.
|
||||
|
||||
The recommended action is to show an HTTP 400 error page.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadStateException(Exception):
|
||||
"""
|
||||
Thrown if all the parameters are correct, but there's no CSRF token in the
|
||||
session. This probably means that the session expired.
|
||||
|
||||
The recommended action is to redirect the user's browser to try the
|
||||
approval process again.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CsrfException(Exception):
|
||||
"""
|
||||
Thrown if the given 'state' parameter doesn't contain the CSRF token from
|
||||
the user's session. This is blocked to prevent CSRF attacks.
|
||||
|
||||
The recommended action is to respond with an HTTP 403 error page.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NotApprovedException(Exception):
|
||||
"""
|
||||
The user chose not to approve your app.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderException(Exception):
|
||||
"""
|
||||
Dropbox redirected to your redirect URI with some unexpected error
|
||||
identifier and error message.
|
||||
|
||||
The recommended action is to log the error, tell the user something went
|
||||
wrong, and let them try again.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _safe_equals(a, b):
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
res = 0
|
||||
for ca, cb in zip(a, b):
|
||||
res |= ord(ca) ^ ord(cb)
|
||||
return res == 0
|
||||
|
||||
|
||||
def _params_to_urlencoded(params):
|
||||
"""
|
||||
Returns a application/x-www-form-urlencoded ``str`` representing the
|
||||
key/value pairs in ``params``.
|
||||
|
||||
Keys are values are ``str()``'d before calling ``urllib.urlencode``, with
|
||||
the exception of unicode objects which are utf8-encoded.
|
||||
"""
|
||||
def encode(o):
|
||||
if isinstance(o, six.binary_type):
|
||||
return o
|
||||
else:
|
||||
if isinstance(o, six.text_type):
|
||||
return o.encode('utf-8')
|
||||
else:
|
||||
return str(o).encode('utf-8')
|
||||
|
||||
utf8_params = {encode(k): encode(v) for k, v in six.iteritems(params)}
|
||||
return url_encode(utf8_params)
|
||||
@@ -1,4 +0,0 @@
|
||||
import resources.lib.utils as utils
|
||||
|
||||
def resource_filename(*args):
|
||||
return utils.addon_dir() + "/resources/lib/dropbox/trusted-certs.crt"
|
||||
@@ -1,835 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
"""
|
||||
This namespace contains helper entities for property and property/template endpoints.
|
||||
"""
|
||||
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
class GetPropertyTemplateArg(object):
|
||||
"""
|
||||
:ivar template_id: An identifier for property template added by route
|
||||
properties/template/add.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_template_id_value',
|
||||
'_template_id_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
template_id=None):
|
||||
self._template_id_value = None
|
||||
self._template_id_present = False
|
||||
if template_id is not None:
|
||||
self.template_id = template_id
|
||||
|
||||
@property
|
||||
def template_id(self):
|
||||
"""
|
||||
An identifier for property template added by route
|
||||
properties/template/add.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._template_id_present:
|
||||
return self._template_id_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'template_id'")
|
||||
|
||||
@template_id.setter
|
||||
def template_id(self, val):
|
||||
val = self._template_id_validator.validate(val)
|
||||
self._template_id_value = val
|
||||
self._template_id_present = True
|
||||
|
||||
@template_id.deleter
|
||||
def template_id(self):
|
||||
self._template_id_value = None
|
||||
self._template_id_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'GetPropertyTemplateArg(template_id={!r})'.format(
|
||||
self._template_id_value,
|
||||
)
|
||||
|
||||
GetPropertyTemplateArg_validator = bv.Struct(GetPropertyTemplateArg)
|
||||
|
||||
class PropertyGroupTemplate(object):
|
||||
"""
|
||||
Describes property templates that can be filled and associated with a file.
|
||||
|
||||
:ivar name: A display name for the property template. Property template
|
||||
names can be up to 256 bytes.
|
||||
:ivar description: Description for new property template. Property template
|
||||
descriptions can be up to 1024 bytes.
|
||||
:ivar fields: This is a list of custom properties associated with a property
|
||||
template. There can be up to 64 properties in a single property
|
||||
template.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_name_value',
|
||||
'_name_present',
|
||||
'_description_value',
|
||||
'_description_present',
|
||||
'_fields_value',
|
||||
'_fields_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
name=None,
|
||||
description=None,
|
||||
fields=None):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
self._description_value = None
|
||||
self._description_present = False
|
||||
self._fields_value = None
|
||||
self._fields_present = False
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if description is not None:
|
||||
self.description = description
|
||||
if fields is not None:
|
||||
self.fields = fields
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
A display name for the property template. Property template names can be
|
||||
up to 256 bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._name_present:
|
||||
return self._name_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'name'")
|
||||
|
||||
@name.setter
|
||||
def name(self, val):
|
||||
val = self._name_validator.validate(val)
|
||||
self._name_value = val
|
||||
self._name_present = True
|
||||
|
||||
@name.deleter
|
||||
def name(self):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Description for new property template. Property template descriptions
|
||||
can be up to 1024 bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._description_present:
|
||||
return self._description_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'description'")
|
||||
|
||||
@description.setter
|
||||
def description(self, val):
|
||||
val = self._description_validator.validate(val)
|
||||
self._description_value = val
|
||||
self._description_present = True
|
||||
|
||||
@description.deleter
|
||||
def description(self):
|
||||
self._description_value = None
|
||||
self._description_present = False
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""
|
||||
This is a list of custom properties associated with a property template.
|
||||
There can be up to 64 properties in a single property template.
|
||||
|
||||
:rtype: list of [PropertyFieldTemplate]
|
||||
"""
|
||||
if self._fields_present:
|
||||
return self._fields_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'fields'")
|
||||
|
||||
@fields.setter
|
||||
def fields(self, val):
|
||||
val = self._fields_validator.validate(val)
|
||||
self._fields_value = val
|
||||
self._fields_present = True
|
||||
|
||||
@fields.deleter
|
||||
def fields(self):
|
||||
self._fields_value = None
|
||||
self._fields_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyGroupTemplate(name={!r}, description={!r}, fields={!r})'.format(
|
||||
self._name_value,
|
||||
self._description_value,
|
||||
self._fields_value,
|
||||
)
|
||||
|
||||
PropertyGroupTemplate_validator = bv.Struct(PropertyGroupTemplate)
|
||||
|
||||
class GetPropertyTemplateResult(PropertyGroupTemplate):
|
||||
"""
|
||||
The Property template for the specified template.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
name=None,
|
||||
description=None,
|
||||
fields=None):
|
||||
super(GetPropertyTemplateResult, self).__init__(name,
|
||||
description,
|
||||
fields)
|
||||
|
||||
def __repr__(self):
|
||||
return 'GetPropertyTemplateResult(name={!r}, description={!r}, fields={!r})'.format(
|
||||
self._name_value,
|
||||
self._description_value,
|
||||
self._fields_value,
|
||||
)
|
||||
|
||||
GetPropertyTemplateResult_validator = bv.Struct(GetPropertyTemplateResult)
|
||||
|
||||
class ListPropertyTemplateIds(object):
|
||||
"""
|
||||
:ivar template_ids: List of identifiers for templates added by route
|
||||
properties/template/add.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_template_ids_value',
|
||||
'_template_ids_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
template_ids=None):
|
||||
self._template_ids_value = None
|
||||
self._template_ids_present = False
|
||||
if template_ids is not None:
|
||||
self.template_ids = template_ids
|
||||
|
||||
@property
|
||||
def template_ids(self):
|
||||
"""
|
||||
List of identifiers for templates added by route
|
||||
properties/template/add.
|
||||
|
||||
:rtype: list of [str]
|
||||
"""
|
||||
if self._template_ids_present:
|
||||
return self._template_ids_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'template_ids'")
|
||||
|
||||
@template_ids.setter
|
||||
def template_ids(self, val):
|
||||
val = self._template_ids_validator.validate(val)
|
||||
self._template_ids_value = val
|
||||
self._template_ids_present = True
|
||||
|
||||
@template_ids.deleter
|
||||
def template_ids(self):
|
||||
self._template_ids_value = None
|
||||
self._template_ids_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'ListPropertyTemplateIds(template_ids={!r})'.format(
|
||||
self._template_ids_value,
|
||||
)
|
||||
|
||||
ListPropertyTemplateIds_validator = bv.Struct(ListPropertyTemplateIds)
|
||||
|
||||
class PropertyTemplateError(bb.Union):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar str template_not_found: Property template does not exist for given
|
||||
identifier.
|
||||
:ivar restricted_content: You do not have the permissions to modify this
|
||||
property template.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
restricted_content = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
@classmethod
|
||||
def template_not_found(cls, val):
|
||||
"""
|
||||
Create an instance of this class set to the ``template_not_found`` tag
|
||||
with value ``val``.
|
||||
|
||||
:param str val:
|
||||
:rtype: PropertyTemplateError
|
||||
"""
|
||||
return cls('template_not_found', val)
|
||||
|
||||
def is_template_not_found(self):
|
||||
"""
|
||||
Check if the union tag is ``template_not_found``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'template_not_found'
|
||||
|
||||
def is_restricted_content(self):
|
||||
"""
|
||||
Check if the union tag is ``restricted_content``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'restricted_content'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def get_template_not_found(self):
|
||||
"""
|
||||
Property template does not exist for given identifier.
|
||||
|
||||
Only call this if :meth:`is_template_not_found` is true.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if not self.is_template_not_found():
|
||||
raise AttributeError("tag 'template_not_found' not set")
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyTemplateError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PropertyTemplateError_validator = bv.Union(PropertyTemplateError)
|
||||
|
||||
class ModifyPropertyTemplateError(PropertyTemplateError):
|
||||
"""
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar conflicting_property_names: A property field name already exists in
|
||||
the template.
|
||||
:ivar too_many_properties: There are too many properties in the changed
|
||||
template. The maximum number of properties per template is 32.
|
||||
:ivar too_many_templates: There are too many templates for the team.
|
||||
:ivar template_attribute_too_large: The template name, description or field
|
||||
names is too large.
|
||||
"""
|
||||
|
||||
# Attribute is overwritten below the class definition
|
||||
conflicting_property_names = None
|
||||
# Attribute is overwritten below the class definition
|
||||
too_many_properties = None
|
||||
# Attribute is overwritten below the class definition
|
||||
too_many_templates = None
|
||||
# Attribute is overwritten below the class definition
|
||||
template_attribute_too_large = None
|
||||
|
||||
def is_conflicting_property_names(self):
|
||||
"""
|
||||
Check if the union tag is ``conflicting_property_names``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'conflicting_property_names'
|
||||
|
||||
def is_too_many_properties(self):
|
||||
"""
|
||||
Check if the union tag is ``too_many_properties``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'too_many_properties'
|
||||
|
||||
def is_too_many_templates(self):
|
||||
"""
|
||||
Check if the union tag is ``too_many_templates``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'too_many_templates'
|
||||
|
||||
def is_template_attribute_too_large(self):
|
||||
"""
|
||||
Check if the union tag is ``template_attribute_too_large``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'template_attribute_too_large'
|
||||
|
||||
def __repr__(self):
|
||||
return 'ModifyPropertyTemplateError(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
ModifyPropertyTemplateError_validator = bv.Union(ModifyPropertyTemplateError)
|
||||
|
||||
class PropertyField(object):
|
||||
"""
|
||||
:ivar name: This is the name or key of a custom property in a property
|
||||
template. File property names can be up to 256 bytes.
|
||||
:ivar value: Value of a custom property attached to a file. Values can be up
|
||||
to 1024 bytes.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_name_value',
|
||||
'_name_present',
|
||||
'_value_value',
|
||||
'_value_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
name=None,
|
||||
value=None):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
self._value_value = None
|
||||
self._value_present = False
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if value is not None:
|
||||
self.value = value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
This is the name or key of a custom property in a property template.
|
||||
File property names can be up to 256 bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._name_present:
|
||||
return self._name_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'name'")
|
||||
|
||||
@name.setter
|
||||
def name(self, val):
|
||||
val = self._name_validator.validate(val)
|
||||
self._name_value = val
|
||||
self._name_present = True
|
||||
|
||||
@name.deleter
|
||||
def name(self):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
Value of a custom property attached to a file. Values can be up to 1024
|
||||
bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._value_present:
|
||||
return self._value_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'value'")
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
val = self._value_validator.validate(val)
|
||||
self._value_value = val
|
||||
self._value_present = True
|
||||
|
||||
@value.deleter
|
||||
def value(self):
|
||||
self._value_value = None
|
||||
self._value_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyField(name={!r}, value={!r})'.format(
|
||||
self._name_value,
|
||||
self._value_value,
|
||||
)
|
||||
|
||||
PropertyField_validator = bv.Struct(PropertyField)
|
||||
|
||||
class PropertyFieldTemplate(object):
|
||||
"""
|
||||
Describe a single property field type which that can be part of a property
|
||||
template.
|
||||
|
||||
:ivar name: This is the name or key of a custom property in a property
|
||||
template. File property names can be up to 256 bytes.
|
||||
:ivar description: This is the description for a custom property in a
|
||||
property template. File property description can be up to 1024 bytes.
|
||||
:ivar type: This is the data type of the value of this property. This type
|
||||
will be enforced upon property creation and modifications.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_name_value',
|
||||
'_name_present',
|
||||
'_description_value',
|
||||
'_description_present',
|
||||
'_type_value',
|
||||
'_type_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
name=None,
|
||||
description=None,
|
||||
type=None):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
self._description_value = None
|
||||
self._description_present = False
|
||||
self._type_value = None
|
||||
self._type_present = False
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if description is not None:
|
||||
self.description = description
|
||||
if type is not None:
|
||||
self.type = type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
This is the name or key of a custom property in a property template.
|
||||
File property names can be up to 256 bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._name_present:
|
||||
return self._name_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'name'")
|
||||
|
||||
@name.setter
|
||||
def name(self, val):
|
||||
val = self._name_validator.validate(val)
|
||||
self._name_value = val
|
||||
self._name_present = True
|
||||
|
||||
@name.deleter
|
||||
def name(self):
|
||||
self._name_value = None
|
||||
self._name_present = False
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
This is the description for a custom property in a property template.
|
||||
File property description can be up to 1024 bytes.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._description_present:
|
||||
return self._description_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'description'")
|
||||
|
||||
@description.setter
|
||||
def description(self, val):
|
||||
val = self._description_validator.validate(val)
|
||||
self._description_value = val
|
||||
self._description_present = True
|
||||
|
||||
@description.deleter
|
||||
def description(self):
|
||||
self._description_value = None
|
||||
self._description_present = False
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""
|
||||
This is the data type of the value of this property. This type will be
|
||||
enforced upon property creation and modifications.
|
||||
|
||||
:rtype: PropertyType
|
||||
"""
|
||||
if self._type_present:
|
||||
return self._type_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'type'")
|
||||
|
||||
@type.setter
|
||||
def type(self, val):
|
||||
self._type_validator.validate_type_only(val)
|
||||
self._type_value = val
|
||||
self._type_present = True
|
||||
|
||||
@type.deleter
|
||||
def type(self):
|
||||
self._type_value = None
|
||||
self._type_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyFieldTemplate(name={!r}, description={!r}, type={!r})'.format(
|
||||
self._name_value,
|
||||
self._description_value,
|
||||
self._type_value,
|
||||
)
|
||||
|
||||
PropertyFieldTemplate_validator = bv.Struct(PropertyFieldTemplate)
|
||||
|
||||
class PropertyGroup(object):
|
||||
"""
|
||||
Collection of custom properties in filled property templates.
|
||||
|
||||
:ivar template_id: A unique identifier for a property template type.
|
||||
:ivar fields: This is a list of custom properties associated with a file.
|
||||
There can be up to 32 properties for a template.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_template_id_value',
|
||||
'_template_id_present',
|
||||
'_fields_value',
|
||||
'_fields_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
template_id=None,
|
||||
fields=None):
|
||||
self._template_id_value = None
|
||||
self._template_id_present = False
|
||||
self._fields_value = None
|
||||
self._fields_present = False
|
||||
if template_id is not None:
|
||||
self.template_id = template_id
|
||||
if fields is not None:
|
||||
self.fields = fields
|
||||
|
||||
@property
|
||||
def template_id(self):
|
||||
"""
|
||||
A unique identifier for a property template type.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._template_id_present:
|
||||
return self._template_id_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'template_id'")
|
||||
|
||||
@template_id.setter
|
||||
def template_id(self, val):
|
||||
val = self._template_id_validator.validate(val)
|
||||
self._template_id_value = val
|
||||
self._template_id_present = True
|
||||
|
||||
@template_id.deleter
|
||||
def template_id(self):
|
||||
self._template_id_value = None
|
||||
self._template_id_present = False
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
"""
|
||||
This is a list of custom properties associated with a file. There can be
|
||||
up to 32 properties for a template.
|
||||
|
||||
:rtype: list of [PropertyField]
|
||||
"""
|
||||
if self._fields_present:
|
||||
return self._fields_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'fields'")
|
||||
|
||||
@fields.setter
|
||||
def fields(self, val):
|
||||
val = self._fields_validator.validate(val)
|
||||
self._fields_value = val
|
||||
self._fields_present = True
|
||||
|
||||
@fields.deleter
|
||||
def fields(self):
|
||||
self._fields_value = None
|
||||
self._fields_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyGroup(template_id={!r}, fields={!r})'.format(
|
||||
self._template_id_value,
|
||||
self._fields_value,
|
||||
)
|
||||
|
||||
PropertyGroup_validator = bv.Struct(PropertyGroup)
|
||||
|
||||
class PropertyType(bb.Union):
|
||||
"""
|
||||
Data type of the given property added. This endpoint is in beta and only
|
||||
properties of type strings is supported.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar string: The associated property will be of type string. Unicode is
|
||||
supported.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
string = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_string(self):
|
||||
"""
|
||||
Check if the union tag is ``string``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'string'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'PropertyType(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
PropertyType_validator = bv.Union(PropertyType)
|
||||
|
||||
TemplateId_validator = bv.String(min_length=1, pattern=u'(/|ptid:).*')
|
||||
GetPropertyTemplateArg._template_id_validator = TemplateId_validator
|
||||
GetPropertyTemplateArg._all_field_names_ = set(['template_id'])
|
||||
GetPropertyTemplateArg._all_fields_ = [('template_id', GetPropertyTemplateArg._template_id_validator)]
|
||||
|
||||
PropertyGroupTemplate._name_validator = bv.String()
|
||||
PropertyGroupTemplate._description_validator = bv.String()
|
||||
PropertyGroupTemplate._fields_validator = bv.List(PropertyFieldTemplate_validator)
|
||||
PropertyGroupTemplate._all_field_names_ = set([
|
||||
'name',
|
||||
'description',
|
||||
'fields',
|
||||
])
|
||||
PropertyGroupTemplate._all_fields_ = [
|
||||
('name', PropertyGroupTemplate._name_validator),
|
||||
('description', PropertyGroupTemplate._description_validator),
|
||||
('fields', PropertyGroupTemplate._fields_validator),
|
||||
]
|
||||
|
||||
GetPropertyTemplateResult._all_field_names_ = PropertyGroupTemplate._all_field_names_.union(set([]))
|
||||
GetPropertyTemplateResult._all_fields_ = PropertyGroupTemplate._all_fields_ + []
|
||||
|
||||
ListPropertyTemplateIds._template_ids_validator = bv.List(TemplateId_validator)
|
||||
ListPropertyTemplateIds._all_field_names_ = set(['template_ids'])
|
||||
ListPropertyTemplateIds._all_fields_ = [('template_ids', ListPropertyTemplateIds._template_ids_validator)]
|
||||
|
||||
PropertyTemplateError._template_not_found_validator = TemplateId_validator
|
||||
PropertyTemplateError._restricted_content_validator = bv.Void()
|
||||
PropertyTemplateError._other_validator = bv.Void()
|
||||
PropertyTemplateError._tagmap = {
|
||||
'template_not_found': PropertyTemplateError._template_not_found_validator,
|
||||
'restricted_content': PropertyTemplateError._restricted_content_validator,
|
||||
'other': PropertyTemplateError._other_validator,
|
||||
}
|
||||
|
||||
PropertyTemplateError.restricted_content = PropertyTemplateError('restricted_content')
|
||||
PropertyTemplateError.other = PropertyTemplateError('other')
|
||||
|
||||
ModifyPropertyTemplateError._conflicting_property_names_validator = bv.Void()
|
||||
ModifyPropertyTemplateError._too_many_properties_validator = bv.Void()
|
||||
ModifyPropertyTemplateError._too_many_templates_validator = bv.Void()
|
||||
ModifyPropertyTemplateError._template_attribute_too_large_validator = bv.Void()
|
||||
ModifyPropertyTemplateError._tagmap = {
|
||||
'conflicting_property_names': ModifyPropertyTemplateError._conflicting_property_names_validator,
|
||||
'too_many_properties': ModifyPropertyTemplateError._too_many_properties_validator,
|
||||
'too_many_templates': ModifyPropertyTemplateError._too_many_templates_validator,
|
||||
'template_attribute_too_large': ModifyPropertyTemplateError._template_attribute_too_large_validator,
|
||||
}
|
||||
ModifyPropertyTemplateError._tagmap.update(PropertyTemplateError._tagmap)
|
||||
|
||||
ModifyPropertyTemplateError.conflicting_property_names = ModifyPropertyTemplateError('conflicting_property_names')
|
||||
ModifyPropertyTemplateError.too_many_properties = ModifyPropertyTemplateError('too_many_properties')
|
||||
ModifyPropertyTemplateError.too_many_templates = ModifyPropertyTemplateError('too_many_templates')
|
||||
ModifyPropertyTemplateError.template_attribute_too_large = ModifyPropertyTemplateError('template_attribute_too_large')
|
||||
|
||||
PropertyField._name_validator = bv.String()
|
||||
PropertyField._value_validator = bv.String()
|
||||
PropertyField._all_field_names_ = set([
|
||||
'name',
|
||||
'value',
|
||||
])
|
||||
PropertyField._all_fields_ = [
|
||||
('name', PropertyField._name_validator),
|
||||
('value', PropertyField._value_validator),
|
||||
]
|
||||
|
||||
PropertyFieldTemplate._name_validator = bv.String()
|
||||
PropertyFieldTemplate._description_validator = bv.String()
|
||||
PropertyFieldTemplate._type_validator = PropertyType_validator
|
||||
PropertyFieldTemplate._all_field_names_ = set([
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
])
|
||||
PropertyFieldTemplate._all_fields_ = [
|
||||
('name', PropertyFieldTemplate._name_validator),
|
||||
('description', PropertyFieldTemplate._description_validator),
|
||||
('type', PropertyFieldTemplate._type_validator),
|
||||
]
|
||||
|
||||
PropertyGroup._template_id_validator = TemplateId_validator
|
||||
PropertyGroup._fields_validator = bv.List(PropertyField_validator)
|
||||
PropertyGroup._all_field_names_ = set([
|
||||
'template_id',
|
||||
'fields',
|
||||
])
|
||||
PropertyGroup._all_fields_ = [
|
||||
('template_id', PropertyGroup._template_id_validator),
|
||||
('fields', PropertyGroup._fields_validator),
|
||||
]
|
||||
|
||||
PropertyType._string_validator = bv.Void()
|
||||
PropertyType._other_validator = bv.Void()
|
||||
PropertyType._tagmap = {
|
||||
'string': PropertyType._string_validator,
|
||||
'other': PropertyType._other_validator,
|
||||
}
|
||||
|
||||
PropertyType.string = PropertyType('string')
|
||||
PropertyType.other = PropertyType('other')
|
||||
|
||||
ROUTES = {
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import pkg_resources
|
||||
import os
|
||||
import ssl
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.poolmanager import PoolManager
|
||||
|
||||
API_DOMAIN = os.environ.get('DROPBOX_API_DOMAIN',
|
||||
os.environ.get('DROPBOX_DOMAIN', '.dropboxapi.com'))
|
||||
|
||||
WEB_DOMAIN = os.environ.get('DROPBOX_WEB_DOMAIN',
|
||||
os.environ.get('DROPBOX_DOMAIN', '.dropbox.com'))
|
||||
|
||||
# Default short hostname for RPC-style routes.
|
||||
HOST_API = 'api'
|
||||
|
||||
# Default short hostname for upload and download-style routes.
|
||||
HOST_CONTENT = 'content'
|
||||
|
||||
# Default short hostname for longpoll routes.
|
||||
HOST_NOTIFY = 'notify'
|
||||
|
||||
# Default short hostname for the Drobox website.
|
||||
HOST_WWW = 'www'
|
||||
|
||||
API_HOST = os.environ.get('DROPBOX_API_HOST', HOST_API + API_DOMAIN)
|
||||
API_CONTENT_HOST = os.environ.get('DROPBOX_API_CONTENT_HOST', HOST_CONTENT + API_DOMAIN)
|
||||
API_NOTIFICATION_HOST = os.environ.get('DROPBOX_API_NOTIFY_HOST', HOST_NOTIFY + API_DOMAIN)
|
||||
WEB_HOST = os.environ.get('DROPBOX_WEB_HOST', HOST_WWW + WEB_DOMAIN)
|
||||
|
||||
_TRUSTED_CERT_FILE = pkg_resources.resource_filename(__name__, 'trusted-certs.crt')
|
||||
|
||||
# TODO(kelkabany): We probably only want to instantiate this once so that even
|
||||
# if multiple Dropbox objects are instantiated, they all share the same pool.
|
||||
class _SSLAdapter(HTTPAdapter):
|
||||
def init_poolmanager(self, connections, maxsize, block=False, **_):
|
||||
self.poolmanager = PoolManager(
|
||||
num_pools=connections,
|
||||
maxsize=maxsize,
|
||||
block=block,
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs=_TRUSTED_CERT_FILE,
|
||||
)
|
||||
|
||||
def pinned_session(pool_maxsize=8):
|
||||
http_adapter = _SSLAdapter(pool_connections=4, pool_maxsize=pool_maxsize)
|
||||
_session = requests.session()
|
||||
_session.mount('https://', http_adapter)
|
||||
|
||||
return _session
|
||||
@@ -1,75 +0,0 @@
|
||||
"""
|
||||
Helpers for representing Stone data types in Python.
|
||||
|
||||
This module should be dropped into a project that requires the use of Stone. In
|
||||
the future, this could be imported from a pre-installed Python package, rather
|
||||
than being added to a project.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv # type: ignore
|
||||
|
||||
_MYPY = False
|
||||
if _MYPY:
|
||||
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
|
||||
|
||||
|
||||
class Union(object):
|
||||
# TODO(kelkabany): Possible optimization is to remove _value if a
|
||||
# union is composed of only symbols.
|
||||
__slots__ = ['_tag', '_value']
|
||||
_tagmap = {} # type: typing.Dict[typing.Text, bv.Validator]
|
||||
|
||||
def __init__(self, tag, value=None):
|
||||
# type: (typing.Text, typing.Optional[typing.Any]) -> None
|
||||
assert tag in self._tagmap, 'Invalid tag %r.' % tag
|
||||
validator = self._tagmap[tag]
|
||||
if isinstance(validator, bv.Void):
|
||||
assert value is None, 'Void type union member must have None value.'
|
||||
elif isinstance(validator, (bv.Struct, bv.Union)):
|
||||
validator.validate_type_only(value)
|
||||
else:
|
||||
validator.validate(value)
|
||||
self._tag = tag
|
||||
self._value = value
|
||||
|
||||
def __eq__(self, other):
|
||||
# Also need to check if one class is a subclass of another. If one union extends another,
|
||||
# the common fields should be able to be compared to each other.
|
||||
return (
|
||||
isinstance(other, Union) and
|
||||
(isinstance(self, other.__class__) or isinstance(other, self.__class__)) and
|
||||
self._tag == other._tag and self._value == other._value
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self._tag, self._value))
|
||||
|
||||
class Route(object):
|
||||
|
||||
def __init__(self, name, deprecated, arg_type, result_type, error_type, attrs):
|
||||
self.name = name
|
||||
self.deprecated = deprecated
|
||||
self.arg_type = arg_type
|
||||
self.result_type = result_type
|
||||
self.error_type = error_type
|
||||
assert isinstance(attrs, dict), 'Expected dict, got %r' % attrs
|
||||
self.attrs = attrs
|
||||
|
||||
def __repr__(self):
|
||||
return 'Route({!r}, {!r}, {!r}, {!r}, {!r}, {!r})'.format(
|
||||
self.name,
|
||||
self.deprecated,
|
||||
self.arg_type,
|
||||
self.result_type,
|
||||
self.error_type,
|
||||
self.attrs)
|
||||
@@ -1,592 +0,0 @@
|
||||
"""
|
||||
Defines classes to represent each Stone type in Python. These classes should
|
||||
be used to validate Python objects and normalize them for a given type.
|
||||
|
||||
The data types defined here should not be specific to an RPC or serialization
|
||||
format.
|
||||
|
||||
This module should be dropped into a project that requires the use of Stone. In
|
||||
the future, this could be imported from a pre-installed Python package, rather
|
||||
than being added to a project.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import datetime
|
||||
import math
|
||||
import numbers
|
||||
import re
|
||||
import six
|
||||
|
||||
_MYPY = False
|
||||
if _MYPY:
|
||||
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
|
||||
|
||||
# See <http://python3porting.com/differences.html#buffer>
|
||||
if six.PY3:
|
||||
_binary_types = (bytes, memoryview) # noqa: E501,F821 # pylint: disable=undefined-variable,useless-suppression
|
||||
else:
|
||||
_binary_types = (bytes, buffer) # noqa: E501,F821 # pylint: disable=undefined-variable,useless-suppression
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Raised when a value doesn't pass validation by its validator."""
|
||||
|
||||
def __init__(self, message, parent=None):
|
||||
"""
|
||||
Args:
|
||||
message (str): Error message detailing validation failure.
|
||||
parent (str): Adds the parent as the closest reference point for
|
||||
the error. Use :meth:`add_parent` to add more.
|
||||
"""
|
||||
super(ValidationError, self).__init__(message)
|
||||
self.message = message
|
||||
self._parents = []
|
||||
if parent:
|
||||
self._parents.append(parent)
|
||||
|
||||
def add_parent(self, parent):
|
||||
"""
|
||||
Args:
|
||||
parent (str): Adds the parent to the top of the tree of references
|
||||
that lead to the validator that failed.
|
||||
"""
|
||||
self._parents.append(parent)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns:
|
||||
str: A descriptive message of the validation error that may also
|
||||
include the path to the validator that failed.
|
||||
"""
|
||||
if self._parents:
|
||||
return '{}: {}'.format('.'.join(self._parents[::-1]), self.message)
|
||||
else:
|
||||
return self.message
|
||||
|
||||
def __repr__(self):
|
||||
# Not a perfect repr, but includes the error location information.
|
||||
return 'ValidationError(%r)' % six.text_type(self)
|
||||
|
||||
|
||||
def generic_type_name(v):
|
||||
"""Return a descriptive type name that isn't Python specific. For example,
|
||||
an int value will return 'integer' rather than 'int'."""
|
||||
if isinstance(v, numbers.Integral):
|
||||
# Must come before real numbers check since integrals are reals too
|
||||
return 'integer'
|
||||
elif isinstance(v, numbers.Real):
|
||||
return 'float'
|
||||
elif isinstance(v, (tuple, list)):
|
||||
return 'list'
|
||||
elif isinstance(v, six.string_types):
|
||||
return 'string'
|
||||
elif v is None:
|
||||
return 'null'
|
||||
else:
|
||||
return type(v).__name__
|
||||
|
||||
|
||||
class Validator(object):
|
||||
"""All primitive and composite data types should be a subclass of this."""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def validate(self, val):
|
||||
"""Validates that val is of this data type.
|
||||
|
||||
Returns: A normalized value if validation succeeds.
|
||||
Raises: ValidationError
|
||||
"""
|
||||
pass
|
||||
|
||||
def has_default(self):
|
||||
return False
|
||||
|
||||
def get_default(self):
|
||||
raise AssertionError('No default available.')
|
||||
|
||||
|
||||
class Primitive(Validator):
|
||||
"""A basic type that is defined by Stone."""
|
||||
# pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
|
||||
class Boolean(Primitive):
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, bool):
|
||||
raise ValidationError('%r is not a valid boolean' % val)
|
||||
return val
|
||||
|
||||
|
||||
class Integer(Primitive):
|
||||
"""
|
||||
Do not use this class directly. Extend it and specify a 'minimum' and
|
||||
'maximum' value as class variables for a more restrictive integer range.
|
||||
"""
|
||||
minimum = None # type: typing.Optional[int]
|
||||
maximum = None # type: typing.Optional[int]
|
||||
|
||||
def __init__(self, min_value=None, max_value=None):
|
||||
"""
|
||||
A more restrictive minimum or maximum value can be specified than the
|
||||
range inherent to the defined type.
|
||||
"""
|
||||
if min_value is not None:
|
||||
assert isinstance(min_value, numbers.Integral), \
|
||||
'min_value must be an integral number'
|
||||
assert min_value >= self.minimum, \
|
||||
'min_value cannot be less than the minimum value for this ' \
|
||||
'type (%d < %d)' % (min_value, self.minimum)
|
||||
self.minimum = min_value
|
||||
if max_value is not None:
|
||||
assert isinstance(max_value, numbers.Integral), \
|
||||
'max_value must be an integral number'
|
||||
assert max_value <= self.maximum, \
|
||||
'max_value cannot be greater than the maximum value for ' \
|
||||
'this type (%d < %d)' % (max_value, self.maximum)
|
||||
self.maximum = max_value
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, numbers.Integral):
|
||||
raise ValidationError('expected integer, got %s'
|
||||
% generic_type_name(val))
|
||||
elif not (self.minimum <= val <= self.maximum):
|
||||
raise ValidationError('%d is not within range [%d, %d]'
|
||||
% (val, self.minimum, self.maximum))
|
||||
return val
|
||||
|
||||
def __repr__(self):
|
||||
return '%s()' % self.__class__.__name__
|
||||
|
||||
|
||||
class Int32(Integer):
|
||||
minimum = -2**31
|
||||
maximum = 2**31 - 1
|
||||
|
||||
|
||||
class UInt32(Integer):
|
||||
minimum = 0
|
||||
maximum = 2**32 - 1
|
||||
|
||||
|
||||
class Int64(Integer):
|
||||
minimum = -2**63
|
||||
maximum = 2**63 - 1
|
||||
|
||||
|
||||
class UInt64(Integer):
|
||||
minimum = 0
|
||||
maximum = 2**64 - 1
|
||||
|
||||
|
||||
class Real(Primitive):
|
||||
"""
|
||||
Do not use this class directly. Extend it and optionally set a 'minimum'
|
||||
and 'maximum' value to enforce a range that's a subset of the Python float
|
||||
implementation. Python floats are doubles.
|
||||
"""
|
||||
minimum = None # type: typing.Optional[float]
|
||||
maximum = None # type: typing.Optional[float]
|
||||
|
||||
def __init__(self, min_value=None, max_value=None):
|
||||
"""
|
||||
A more restrictive minimum or maximum value can be specified than the
|
||||
range inherent to the defined type.
|
||||
"""
|
||||
if min_value is not None:
|
||||
assert isinstance(min_value, numbers.Real), \
|
||||
'min_value must be a real number'
|
||||
if not isinstance(min_value, float):
|
||||
try:
|
||||
min_value = float(min_value)
|
||||
except OverflowError:
|
||||
raise AssertionError('min_value is too small for a float')
|
||||
if self.minimum is not None and min_value < self.minimum:
|
||||
raise AssertionError('min_value cannot be less than the '
|
||||
'minimum value for this type (%f < %f)' %
|
||||
(min_value, self.minimum))
|
||||
self.minimum = min_value
|
||||
if max_value is not None:
|
||||
assert isinstance(max_value, numbers.Real), \
|
||||
'max_value must be a real number'
|
||||
if not isinstance(max_value, float):
|
||||
try:
|
||||
max_value = float(max_value)
|
||||
except OverflowError:
|
||||
raise AssertionError('max_value is too large for a float')
|
||||
if self.maximum is not None and max_value > self.maximum:
|
||||
raise AssertionError('max_value cannot be greater than the '
|
||||
'maximum value for this type (%f < %f)' %
|
||||
(max_value, self.maximum))
|
||||
self.maximum = max_value
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, numbers.Real):
|
||||
raise ValidationError('expected real number, got %s' %
|
||||
generic_type_name(val))
|
||||
if not isinstance(val, float):
|
||||
# This checks for the case where a number is passed in with a
|
||||
# magnitude larger than supported by float64.
|
||||
try:
|
||||
val = float(val)
|
||||
except OverflowError:
|
||||
raise ValidationError('too large for float')
|
||||
if math.isnan(val) or math.isinf(val):
|
||||
raise ValidationError('%f values are not supported' % val)
|
||||
if self.minimum is not None and val < self.minimum:
|
||||
raise ValidationError('%f is not greater than %f' %
|
||||
(val, self.minimum))
|
||||
if self.maximum is not None and val > self.maximum:
|
||||
raise ValidationError('%f is not less than %f' %
|
||||
(val, self.maximum))
|
||||
return val
|
||||
|
||||
def __repr__(self):
|
||||
return '%s()' % self.__class__.__name__
|
||||
|
||||
|
||||
class Float32(Real):
|
||||
# Maximum and minimums from the IEEE 754-1985 standard
|
||||
minimum = -3.40282 * 10**38
|
||||
maximum = 3.40282 * 10**38
|
||||
|
||||
|
||||
class Float64(Real):
|
||||
pass
|
||||
|
||||
|
||||
class String(Primitive):
|
||||
"""Represents a unicode string."""
|
||||
|
||||
def __init__(self, min_length=None, max_length=None, pattern=None):
|
||||
if min_length is not None:
|
||||
assert isinstance(min_length, numbers.Integral), \
|
||||
'min_length must be an integral number'
|
||||
assert min_length >= 0, 'min_length must be >= 0'
|
||||
if max_length is not None:
|
||||
assert isinstance(max_length, numbers.Integral), \
|
||||
'max_length must be an integral number'
|
||||
assert max_length > 0, 'max_length must be > 0'
|
||||
if min_length and max_length:
|
||||
assert max_length >= min_length, 'max_length must be >= min_length'
|
||||
if pattern is not None:
|
||||
assert isinstance(pattern, six.string_types), \
|
||||
'pattern must be a string'
|
||||
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
self.pattern = pattern
|
||||
self.pattern_re = None
|
||||
|
||||
if pattern:
|
||||
try:
|
||||
self.pattern_re = re.compile(r"\A(?:" + pattern + r")\Z")
|
||||
except re.error as e:
|
||||
raise AssertionError('Regex {!r} failed: {}'.format(
|
||||
pattern, e.args[0]))
|
||||
|
||||
def validate(self, val):
|
||||
"""
|
||||
A unicode string of the correct length and pattern will pass validation.
|
||||
In PY2, we enforce that a str type must be valid utf-8, and a unicode
|
||||
string will be returned.
|
||||
"""
|
||||
if not isinstance(val, six.string_types):
|
||||
raise ValidationError("'%s' expected to be a string, got %s"
|
||||
% (val, generic_type_name(val)))
|
||||
if not six.PY3 and isinstance(val, str):
|
||||
try:
|
||||
val = val.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
raise ValidationError("'%s' was not valid utf-8")
|
||||
|
||||
if self.max_length is not None and len(val) > self.max_length:
|
||||
raise ValidationError("'%s' must be at most %d characters, got %d"
|
||||
% (val, self.max_length, len(val)))
|
||||
if self.min_length is not None and len(val) < self.min_length:
|
||||
raise ValidationError("'%s' must be at least %d characters, got %d"
|
||||
% (val, self.min_length, len(val)))
|
||||
|
||||
if self.pattern and not self.pattern_re.match(val):
|
||||
raise ValidationError("'%s' did not match pattern '%s'"
|
||||
% (val, self.pattern))
|
||||
return val
|
||||
|
||||
|
||||
class Bytes(Primitive):
|
||||
|
||||
def __init__(self, min_length=None, max_length=None):
|
||||
if min_length is not None:
|
||||
assert isinstance(min_length, numbers.Integral), \
|
||||
'min_length must be an integral number'
|
||||
assert min_length >= 0, 'min_length must be >= 0'
|
||||
if max_length is not None:
|
||||
assert isinstance(max_length, numbers.Integral), \
|
||||
'max_length must be an integral number'
|
||||
assert max_length > 0, 'max_length must be > 0'
|
||||
if min_length is not None and max_length is not None:
|
||||
assert max_length >= min_length, 'max_length must be >= min_length'
|
||||
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, _binary_types):
|
||||
raise ValidationError("expected bytes type, got %s"
|
||||
% generic_type_name(val))
|
||||
elif self.max_length is not None and len(val) > self.max_length:
|
||||
raise ValidationError("'%s' must have at most %d bytes, got %d"
|
||||
% (val, self.max_length, len(val)))
|
||||
elif self.min_length is not None and len(val) < self.min_length:
|
||||
raise ValidationError("'%s' has fewer than %d bytes, got %d"
|
||||
% (val, self.min_length, len(val)))
|
||||
return val
|
||||
|
||||
|
||||
class Timestamp(Primitive):
|
||||
"""Note that while a format is specified, it isn't used in validation
|
||||
since a native Python datetime object is preferred. The format, however,
|
||||
can and should be used by serializers."""
|
||||
|
||||
def __init__(self, fmt):
|
||||
"""fmt must be composed of format codes that the C standard (1989)
|
||||
supports, most notably in its strftime() function."""
|
||||
assert isinstance(fmt, six.text_type), 'format must be a string'
|
||||
self.format = fmt
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, datetime.datetime):
|
||||
raise ValidationError('expected timestamp, got %s'
|
||||
% generic_type_name(val))
|
||||
elif val.tzinfo is not None and \
|
||||
val.tzinfo.utcoffset(val).total_seconds() != 0:
|
||||
raise ValidationError('timestamp should have either a UTC '
|
||||
'timezone or none set at all')
|
||||
return val
|
||||
|
||||
|
||||
class Composite(Validator):
|
||||
"""Validator for a type that builds on other primitive and composite
|
||||
types."""
|
||||
# pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
|
||||
class List(Composite):
|
||||
"""Assumes list contents are homogeneous with respect to types."""
|
||||
|
||||
def __init__(self, item_validator, min_items=None, max_items=None):
|
||||
"""Every list item will be validated with item_validator."""
|
||||
self.item_validator = item_validator
|
||||
if min_items is not None:
|
||||
assert isinstance(min_items, numbers.Integral), \
|
||||
'min_items must be an integral number'
|
||||
assert min_items >= 0, 'min_items must be >= 0'
|
||||
if max_items is not None:
|
||||
assert isinstance(max_items, numbers.Integral), \
|
||||
'max_items must be an integral number'
|
||||
assert max_items > 0, 'max_items must be > 0'
|
||||
if min_items is not None and max_items is not None:
|
||||
assert max_items >= min_items, 'max_items must be >= min_items'
|
||||
|
||||
self.min_items = min_items
|
||||
self.max_items = max_items
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, (tuple, list)):
|
||||
raise ValidationError('%r is not a valid list' % val)
|
||||
elif self.max_items is not None and len(val) > self.max_items:
|
||||
raise ValidationError('%r has more than %s items'
|
||||
% (val, self.max_items))
|
||||
elif self.min_items is not None and len(val) < self.min_items:
|
||||
raise ValidationError('%r has fewer than %s items'
|
||||
% (val, self.min_items))
|
||||
return [self.item_validator.validate(item) for item in val]
|
||||
|
||||
|
||||
class Map(Composite):
|
||||
"""Assumes map keys and values are homogeneous with respect to types."""
|
||||
|
||||
def __init__(self, key_validator, value_validator):
|
||||
"""
|
||||
Every Map key/value pair will be validated with item_validator.
|
||||
key validators must be a subclass of a String validator
|
||||
"""
|
||||
self.key_validator = key_validator
|
||||
self.value_validator = value_validator
|
||||
|
||||
def validate(self, val):
|
||||
if not isinstance(val, dict):
|
||||
raise ValidationError('%r is not a valid dict' % val)
|
||||
return {
|
||||
self.key_validator.validate(key):
|
||||
self.value_validator.validate(value) for key, value in val.items()
|
||||
}
|
||||
|
||||
|
||||
class Struct(Composite):
|
||||
|
||||
def __init__(self, definition):
|
||||
"""
|
||||
Args:
|
||||
definition (class): A generated class representing a Stone struct
|
||||
from a spec. Must have a _fields_ attribute with the following
|
||||
structure:
|
||||
|
||||
_fields_ = [(field_name, validator), ...]
|
||||
|
||||
where
|
||||
field_name: Name of the field (str).
|
||||
validator: Validator object.
|
||||
"""
|
||||
super(Struct, self).__init__()
|
||||
self.definition = definition
|
||||
|
||||
def validate(self, val):
|
||||
"""
|
||||
For a val to pass validation, val must be of the correct type and have
|
||||
all required fields present.
|
||||
"""
|
||||
self.validate_type_only(val)
|
||||
self.validate_fields_only(val)
|
||||
return val
|
||||
|
||||
def validate_fields_only(self, val):
|
||||
"""
|
||||
To pass field validation, no required field should be missing.
|
||||
|
||||
This method assumes that the contents of each field have already been
|
||||
validated on assignment, so it's merely a presence check.
|
||||
|
||||
FIXME(kelkabany): Since the definition object does not maintain a list
|
||||
of which fields are required, all fields are scanned.
|
||||
"""
|
||||
for field_name, _ in self.definition._all_fields_:
|
||||
if not hasattr(val, field_name):
|
||||
raise ValidationError("missing required field '%s'" %
|
||||
field_name)
|
||||
|
||||
def validate_type_only(self, val):
|
||||
"""
|
||||
Use this when you only want to validate that the type of an object
|
||||
is correct, but not yet validate each field.
|
||||
"""
|
||||
# Since the definition maintains the list of fields for serialization,
|
||||
# we're okay with a subclass that might have extra information. This
|
||||
# makes it easier to return one subclass for two routes, one of which
|
||||
# relies on the parent class.
|
||||
if not isinstance(val, self.definition):
|
||||
raise ValidationError('expected type %s, got %s' %
|
||||
(self.definition.__name__, generic_type_name(val)))
|
||||
|
||||
def has_default(self):
|
||||
return not self.definition._has_required_fields
|
||||
|
||||
def get_default(self):
|
||||
assert not self.definition._has_required_fields, 'No default available.'
|
||||
return self.definition()
|
||||
|
||||
|
||||
class StructTree(Struct):
|
||||
"""Validator for structs with enumerated subtypes.
|
||||
|
||||
NOTE: validate_fields_only() validates the fields known to this base
|
||||
struct, but does not do any validation specific to the subtype.
|
||||
"""
|
||||
|
||||
# See PyCQA/pylint#1043 for why this is disabled; this should show up
|
||||
# as a usless-suppression (and can be removed) once a fix is released
|
||||
def __init__(self, definition): # pylint: disable=useless-super-delegation
|
||||
super(StructTree, self).__init__(definition)
|
||||
|
||||
|
||||
class Union(Composite):
|
||||
|
||||
def __init__(self, definition):
|
||||
"""
|
||||
Args:
|
||||
definition (class): A generated class representing a Stone union
|
||||
from a spec. Must have a _tagmap attribute with the following
|
||||
structure:
|
||||
|
||||
_tagmap = {field_name: validator, ...}
|
||||
|
||||
where
|
||||
field_name (str): Tag name.
|
||||
validator (Validator): Tag value validator.
|
||||
"""
|
||||
self.definition = definition
|
||||
|
||||
def validate(self, val):
|
||||
"""
|
||||
For a val to pass validation, it must have a _tag set. This assumes
|
||||
that the object validated that _tag is a valid tag, and that any
|
||||
associated value has also been validated.
|
||||
"""
|
||||
self.validate_type_only(val)
|
||||
if not hasattr(val, '_tag') or val._tag is None:
|
||||
raise ValidationError('no tag set')
|
||||
return val
|
||||
|
||||
def validate_type_only(self, val):
|
||||
"""
|
||||
Use this when you only want to validate that the type of an object
|
||||
is correct, but not yet validate each field.
|
||||
|
||||
We check whether val is a Python parent class of the definition. This
|
||||
is because Union subtyping works in the opposite direction of Python
|
||||
inheritance. For example, if a union U2 extends U1 in Python, this
|
||||
validator will accept U1 in places where U2 is expected.
|
||||
"""
|
||||
if not issubclass(self.definition, type(val)):
|
||||
raise ValidationError('expected type %s or subtype, got %s' %
|
||||
(self.definition.__name__, generic_type_name(val)))
|
||||
|
||||
|
||||
class Void(Primitive):
|
||||
|
||||
def validate(self, val):
|
||||
if val is not None:
|
||||
raise ValidationError('expected NoneType, got %s' %
|
||||
generic_type_name(val))
|
||||
|
||||
def has_default(self):
|
||||
return True
|
||||
|
||||
def get_default(self):
|
||||
return None
|
||||
|
||||
|
||||
class Nullable(Validator):
|
||||
|
||||
def __init__(self, validator):
|
||||
assert isinstance(validator, (Primitive, Composite)), \
|
||||
'validator must be for a primitive or composite type'
|
||||
assert not isinstance(validator, Nullable), \
|
||||
'nullables cannot be stacked'
|
||||
assert not isinstance(validator, Void), \
|
||||
'void cannot be made nullable'
|
||||
self.validator = validator
|
||||
|
||||
def validate(self, val):
|
||||
if val is None:
|
||||
return
|
||||
else:
|
||||
return self.validator.validate(val)
|
||||
|
||||
def validate_type_only(self, val):
|
||||
"""Use this only if Nullable is wrapping a Composite."""
|
||||
if val is None:
|
||||
return
|
||||
else:
|
||||
return self.validator.validate_type_only(val)
|
||||
|
||||
def has_default(self):
|
||||
return True
|
||||
|
||||
def get_default(self):
|
||||
return None
|
||||
@@ -1,468 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
try:
|
||||
from . import (
|
||||
common,
|
||||
)
|
||||
except (SystemError, ValueError):
|
||||
import common
|
||||
|
||||
class GroupManagementType(bb.Union):
|
||||
"""
|
||||
The group type determines how a group is managed.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar user_managed: A group which is managed by selected users.
|
||||
:ivar company_managed: A group which is managed by team admins only.
|
||||
:ivar system_managed: A group which is managed automatically by Dropbox.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
user_managed = None
|
||||
# Attribute is overwritten below the class definition
|
||||
company_managed = None
|
||||
# Attribute is overwritten below the class definition
|
||||
system_managed = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_user_managed(self):
|
||||
"""
|
||||
Check if the union tag is ``user_managed``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'user_managed'
|
||||
|
||||
def is_company_managed(self):
|
||||
"""
|
||||
Check if the union tag is ``company_managed``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'company_managed'
|
||||
|
||||
def is_system_managed(self):
|
||||
"""
|
||||
Check if the union tag is ``system_managed``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'system_managed'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'GroupManagementType(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
GroupManagementType_validator = bv.Union(GroupManagementType)
|
||||
|
||||
class GroupSummary(object):
|
||||
"""
|
||||
Information about a group.
|
||||
|
||||
:ivar group_external_id: External ID of group. This is an arbitrary ID that
|
||||
an admin can attach to a group.
|
||||
:ivar member_count: The number of members in the group.
|
||||
:ivar group_management_type: Who is allowed to manage the group.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_group_name_value',
|
||||
'_group_name_present',
|
||||
'_group_id_value',
|
||||
'_group_id_present',
|
||||
'_group_external_id_value',
|
||||
'_group_external_id_present',
|
||||
'_member_count_value',
|
||||
'_member_count_present',
|
||||
'_group_management_type_value',
|
||||
'_group_management_type_present',
|
||||
]
|
||||
|
||||
_has_required_fields = True
|
||||
|
||||
def __init__(self,
|
||||
group_name=None,
|
||||
group_id=None,
|
||||
group_management_type=None,
|
||||
group_external_id=None,
|
||||
member_count=None):
|
||||
self._group_name_value = None
|
||||
self._group_name_present = False
|
||||
self._group_id_value = None
|
||||
self._group_id_present = False
|
||||
self._group_external_id_value = None
|
||||
self._group_external_id_present = False
|
||||
self._member_count_value = None
|
||||
self._member_count_present = False
|
||||
self._group_management_type_value = None
|
||||
self._group_management_type_present = False
|
||||
if group_name is not None:
|
||||
self.group_name = group_name
|
||||
if group_id is not None:
|
||||
self.group_id = group_id
|
||||
if group_external_id is not None:
|
||||
self.group_external_id = group_external_id
|
||||
if member_count is not None:
|
||||
self.member_count = member_count
|
||||
if group_management_type is not None:
|
||||
self.group_management_type = group_management_type
|
||||
|
||||
@property
|
||||
def group_name(self):
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
if self._group_name_present:
|
||||
return self._group_name_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'group_name'")
|
||||
|
||||
@group_name.setter
|
||||
def group_name(self, val):
|
||||
val = self._group_name_validator.validate(val)
|
||||
self._group_name_value = val
|
||||
self._group_name_present = True
|
||||
|
||||
@group_name.deleter
|
||||
def group_name(self):
|
||||
self._group_name_value = None
|
||||
self._group_name_present = False
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
if self._group_id_present:
|
||||
return self._group_id_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'group_id'")
|
||||
|
||||
@group_id.setter
|
||||
def group_id(self, val):
|
||||
val = self._group_id_validator.validate(val)
|
||||
self._group_id_value = val
|
||||
self._group_id_present = True
|
||||
|
||||
@group_id.deleter
|
||||
def group_id(self):
|
||||
self._group_id_value = None
|
||||
self._group_id_present = False
|
||||
|
||||
@property
|
||||
def group_external_id(self):
|
||||
"""
|
||||
External ID of group. This is an arbitrary ID that an admin can attach
|
||||
to a group.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._group_external_id_present:
|
||||
return self._group_external_id_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@group_external_id.setter
|
||||
def group_external_id(self, val):
|
||||
if val is None:
|
||||
del self.group_external_id
|
||||
return
|
||||
val = self._group_external_id_validator.validate(val)
|
||||
self._group_external_id_value = val
|
||||
self._group_external_id_present = True
|
||||
|
||||
@group_external_id.deleter
|
||||
def group_external_id(self):
|
||||
self._group_external_id_value = None
|
||||
self._group_external_id_present = False
|
||||
|
||||
@property
|
||||
def member_count(self):
|
||||
"""
|
||||
The number of members in the group.
|
||||
|
||||
:rtype: long
|
||||
"""
|
||||
if self._member_count_present:
|
||||
return self._member_count_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@member_count.setter
|
||||
def member_count(self, val):
|
||||
if val is None:
|
||||
del self.member_count
|
||||
return
|
||||
val = self._member_count_validator.validate(val)
|
||||
self._member_count_value = val
|
||||
self._member_count_present = True
|
||||
|
||||
@member_count.deleter
|
||||
def member_count(self):
|
||||
self._member_count_value = None
|
||||
self._member_count_present = False
|
||||
|
||||
@property
|
||||
def group_management_type(self):
|
||||
"""
|
||||
Who is allowed to manage the group.
|
||||
|
||||
:rtype: GroupManagementType
|
||||
"""
|
||||
if self._group_management_type_present:
|
||||
return self._group_management_type_value
|
||||
else:
|
||||
raise AttributeError("missing required field 'group_management_type'")
|
||||
|
||||
@group_management_type.setter
|
||||
def group_management_type(self, val):
|
||||
self._group_management_type_validator.validate_type_only(val)
|
||||
self._group_management_type_value = val
|
||||
self._group_management_type_present = True
|
||||
|
||||
@group_management_type.deleter
|
||||
def group_management_type(self):
|
||||
self._group_management_type_value = None
|
||||
self._group_management_type_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'GroupSummary(group_name={!r}, group_id={!r}, group_management_type={!r}, group_external_id={!r}, member_count={!r})'.format(
|
||||
self._group_name_value,
|
||||
self._group_id_value,
|
||||
self._group_management_type_value,
|
||||
self._group_external_id_value,
|
||||
self._member_count_value,
|
||||
)
|
||||
|
||||
GroupSummary_validator = bv.Struct(GroupSummary)
|
||||
|
||||
class GroupType(bb.Union):
|
||||
"""
|
||||
The group type determines how a group is created and managed.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar team: A group to which team members are automatically added.
|
||||
Applicable to `team folders <https://www.dropbox.com/help/986>`_ only.
|
||||
:ivar user_managed: A group is created and managed by a user.
|
||||
"""
|
||||
|
||||
_catch_all = 'other'
|
||||
# Attribute is overwritten below the class definition
|
||||
team = None
|
||||
# Attribute is overwritten below the class definition
|
||||
user_managed = None
|
||||
# Attribute is overwritten below the class definition
|
||||
other = None
|
||||
|
||||
def is_team(self):
|
||||
"""
|
||||
Check if the union tag is ``team``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'team'
|
||||
|
||||
def is_user_managed(self):
|
||||
"""
|
||||
Check if the union tag is ``user_managed``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'user_managed'
|
||||
|
||||
def is_other(self):
|
||||
"""
|
||||
Check if the union tag is ``other``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'other'
|
||||
|
||||
def __repr__(self):
|
||||
return 'GroupType(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
GroupType_validator = bv.Union(GroupType)
|
||||
|
||||
class TimeRange(object):
|
||||
"""
|
||||
Time range.
|
||||
|
||||
:ivar start_time: Optional starting time (inclusive).
|
||||
:ivar end_time: Optional ending time (exclusive).
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
'_start_time_value',
|
||||
'_start_time_present',
|
||||
'_end_time_value',
|
||||
'_end_time_present',
|
||||
]
|
||||
|
||||
_has_required_fields = False
|
||||
|
||||
def __init__(self,
|
||||
start_time=None,
|
||||
end_time=None):
|
||||
self._start_time_value = None
|
||||
self._start_time_present = False
|
||||
self._end_time_value = None
|
||||
self._end_time_present = False
|
||||
if start_time is not None:
|
||||
self.start_time = start_time
|
||||
if end_time is not None:
|
||||
self.end_time = end_time
|
||||
|
||||
@property
|
||||
def start_time(self):
|
||||
"""
|
||||
Optional starting time (inclusive).
|
||||
|
||||
:rtype: datetime.datetime
|
||||
"""
|
||||
if self._start_time_present:
|
||||
return self._start_time_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@start_time.setter
|
||||
def start_time(self, val):
|
||||
if val is None:
|
||||
del self.start_time
|
||||
return
|
||||
val = self._start_time_validator.validate(val)
|
||||
self._start_time_value = val
|
||||
self._start_time_present = True
|
||||
|
||||
@start_time.deleter
|
||||
def start_time(self):
|
||||
self._start_time_value = None
|
||||
self._start_time_present = False
|
||||
|
||||
@property
|
||||
def end_time(self):
|
||||
"""
|
||||
Optional ending time (exclusive).
|
||||
|
||||
:rtype: datetime.datetime
|
||||
"""
|
||||
if self._end_time_present:
|
||||
return self._end_time_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@end_time.setter
|
||||
def end_time(self, val):
|
||||
if val is None:
|
||||
del self.end_time
|
||||
return
|
||||
val = self._end_time_validator.validate(val)
|
||||
self._end_time_value = val
|
||||
self._end_time_present = True
|
||||
|
||||
@end_time.deleter
|
||||
def end_time(self):
|
||||
self._end_time_value = None
|
||||
self._end_time_present = False
|
||||
|
||||
def __repr__(self):
|
||||
return 'TimeRange(start_time={!r}, end_time={!r})'.format(
|
||||
self._start_time_value,
|
||||
self._end_time_value,
|
||||
)
|
||||
|
||||
TimeRange_validator = bv.Struct(TimeRange)
|
||||
|
||||
GroupExternalId_validator = bv.String()
|
||||
GroupId_validator = bv.String()
|
||||
MemberExternalId_validator = bv.String(max_length=64)
|
||||
ResellerId_validator = bv.String()
|
||||
TeamMemberId_validator = bv.String()
|
||||
GroupManagementType._user_managed_validator = bv.Void()
|
||||
GroupManagementType._company_managed_validator = bv.Void()
|
||||
GroupManagementType._system_managed_validator = bv.Void()
|
||||
GroupManagementType._other_validator = bv.Void()
|
||||
GroupManagementType._tagmap = {
|
||||
'user_managed': GroupManagementType._user_managed_validator,
|
||||
'company_managed': GroupManagementType._company_managed_validator,
|
||||
'system_managed': GroupManagementType._system_managed_validator,
|
||||
'other': GroupManagementType._other_validator,
|
||||
}
|
||||
|
||||
GroupManagementType.user_managed = GroupManagementType('user_managed')
|
||||
GroupManagementType.company_managed = GroupManagementType('company_managed')
|
||||
GroupManagementType.system_managed = GroupManagementType('system_managed')
|
||||
GroupManagementType.other = GroupManagementType('other')
|
||||
|
||||
GroupSummary._group_name_validator = bv.String()
|
||||
GroupSummary._group_id_validator = GroupId_validator
|
||||
GroupSummary._group_external_id_validator = bv.Nullable(GroupExternalId_validator)
|
||||
GroupSummary._member_count_validator = bv.Nullable(bv.UInt32())
|
||||
GroupSummary._group_management_type_validator = GroupManagementType_validator
|
||||
GroupSummary._all_field_names_ = set([
|
||||
'group_name',
|
||||
'group_id',
|
||||
'group_external_id',
|
||||
'member_count',
|
||||
'group_management_type',
|
||||
])
|
||||
GroupSummary._all_fields_ = [
|
||||
('group_name', GroupSummary._group_name_validator),
|
||||
('group_id', GroupSummary._group_id_validator),
|
||||
('group_external_id', GroupSummary._group_external_id_validator),
|
||||
('member_count', GroupSummary._member_count_validator),
|
||||
('group_management_type', GroupSummary._group_management_type_validator),
|
||||
]
|
||||
|
||||
GroupType._team_validator = bv.Void()
|
||||
GroupType._user_managed_validator = bv.Void()
|
||||
GroupType._other_validator = bv.Void()
|
||||
GroupType._tagmap = {
|
||||
'team': GroupType._team_validator,
|
||||
'user_managed': GroupType._user_managed_validator,
|
||||
'other': GroupType._other_validator,
|
||||
}
|
||||
|
||||
GroupType.team = GroupType('team')
|
||||
GroupType.user_managed = GroupType('user_managed')
|
||||
GroupType.other = GroupType('other')
|
||||
|
||||
TimeRange._start_time_validator = bv.Nullable(common.DropboxTimestamp_validator)
|
||||
TimeRange._end_time_validator = bv.Nullable(common.DropboxTimestamp_validator)
|
||||
TimeRange._all_field_names_ = set([
|
||||
'start_time',
|
||||
'end_time',
|
||||
])
|
||||
TimeRange._all_fields_ = [
|
||||
('start_time', TimeRange._start_time_validator),
|
||||
('end_time', TimeRange._end_time_validator),
|
||||
]
|
||||
|
||||
ROUTES = {
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
"""
|
||||
This namespace contains common data types used within the users namespace.
|
||||
"""
|
||||
|
||||
try:
|
||||
from . import stone_validators as bv
|
||||
from . import stone_base as bb
|
||||
except (SystemError, ValueError):
|
||||
# Catch errors raised when importing a relative module when not in a package.
|
||||
# This makes testing this file directly (outside of a package) easier.
|
||||
import stone_validators as bv
|
||||
import stone_base as bb
|
||||
|
||||
class AccountType(bb.Union):
|
||||
"""
|
||||
What type of account this user has.
|
||||
|
||||
This class acts as a tagged union. Only one of the ``is_*`` methods will
|
||||
return true. To get the associated value of a tag (if one exists), use the
|
||||
corresponding ``get_*`` method.
|
||||
|
||||
:ivar basic: The basic account type.
|
||||
:ivar pro: The Dropbox Pro account type.
|
||||
:ivar business: The Dropbox Business account type.
|
||||
"""
|
||||
|
||||
_catch_all = None
|
||||
# Attribute is overwritten below the class definition
|
||||
basic = None
|
||||
# Attribute is overwritten below the class definition
|
||||
pro = None
|
||||
# Attribute is overwritten below the class definition
|
||||
business = None
|
||||
|
||||
def is_basic(self):
|
||||
"""
|
||||
Check if the union tag is ``basic``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'basic'
|
||||
|
||||
def is_pro(self):
|
||||
"""
|
||||
Check if the union tag is ``pro``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'pro'
|
||||
|
||||
def is_business(self):
|
||||
"""
|
||||
Check if the union tag is ``business``.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self._tag == 'business'
|
||||
|
||||
def __repr__(self):
|
||||
return 'AccountType(%r, %r)' % (self._tag, self._value)
|
||||
|
||||
AccountType_validator = bv.Union(AccountType)
|
||||
|
||||
AccountId_validator = bv.String(min_length=40, max_length=40)
|
||||
AccountType._basic_validator = bv.Void()
|
||||
AccountType._pro_validator = bv.Void()
|
||||
AccountType._business_validator = bv.Void()
|
||||
AccountType._tagmap = {
|
||||
'basic': AccountType._basic_validator,
|
||||
'pro': AccountType._pro_validator,
|
||||
'business': AccountType._business_validator,
|
||||
}
|
||||
|
||||
AccountType.basic = AccountType('basic')
|
||||
AccountType.pro = AccountType('pro')
|
||||
AccountType.business = AccountType('business')
|
||||
|
||||
ROUTES = {
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import utils as utils
|
||||
from . import utils as utils
|
||||
|
||||
|
||||
class ZipExtractor:
|
||||
|
||||
def extract(self,zipFile,outLoc,progressBar):
|
||||
|
||||
def extract(self, zipFile, outLoc, progressBar):
|
||||
utils.log("extracting zip archive")
|
||||
|
||||
result = True #result is true unless we fail
|
||||
|
||||
#update the progress bar
|
||||
progressBar.updateProgress(0,utils.getString(30100))
|
||||
|
||||
#list the files
|
||||
|
||||
result = True # result is true unless we fail
|
||||
|
||||
# update the progress bar
|
||||
progressBar.updateProgress(0, utils.getString(30100))
|
||||
|
||||
# list the files
|
||||
fileCount = float(len(zipFile.listFiles()))
|
||||
currentFile = 0
|
||||
|
||||
|
||||
try:
|
||||
for aFile in zipFile.listFiles():
|
||||
#update the progress bar
|
||||
# update the progress bar
|
||||
currentFile += 1
|
||||
progressBar.updateProgress(int((currentFile/fileCount) * 100),utils.getString(30100))
|
||||
|
||||
#extract the file
|
||||
zipFile.extract(aFile,outLoc)
|
||||
|
||||
except Exception as e:
|
||||
progressBar.updateProgress(int((currentFile / fileCount) * 100), utils.getString(30100))
|
||||
|
||||
# extract the file
|
||||
zipFile.extract(aFile, outLoc)
|
||||
|
||||
except Exception:
|
||||
utils.log("Error extracting file")
|
||||
result = False
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,99 +1,47 @@
|
||||
import utils as utils
|
||||
from xml.dom import minidom
|
||||
from xml.parsers.expat import ExpatError
|
||||
import json
|
||||
import xbmc,xbmcvfs
|
||||
import xbmc
|
||||
from . import utils as utils
|
||||
|
||||
|
||||
class GuiSettingsManager:
|
||||
settingsFile = None
|
||||
doc = None
|
||||
settings_allowed = list()
|
||||
found_settings = list()
|
||||
|
||||
def __init__(self,settingsFile):
|
||||
self._readFile(xbmc.translatePath(settingsFile))
|
||||
|
||||
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"}}'))
|
||||
|
||||
settings = json_response['result']['settings']
|
||||
|
||||
for aSetting in settings:
|
||||
self.settings_allowed.append(aSetting['id'])
|
||||
|
||||
#parse the existing xml file and get all the settings
|
||||
root_nodes = self.__parseNodes(self.doc.documentElement)
|
||||
|
||||
for aNode in root_nodes:
|
||||
secondary_list = self.__parseNodes(self.doc.getElementsByTagName(aNode.name)[0])
|
||||
|
||||
for secondNode in secondary_list:
|
||||
#if the node does not have children and is not default
|
||||
if(not secondNode.hasChildren and not secondNode.isDefault):
|
||||
|
||||
if(secondNode.json_name() in self.settings_allowed):
|
||||
self.found_settings.append(secondNode)
|
||||
|
||||
#go through all the found settings and update them
|
||||
for aSetting in self.found_settings:
|
||||
utils.log("updating: " + aSetting.json_name() + ", value: " + aSetting.value)
|
||||
|
||||
#check for boolean and numeric values
|
||||
if(aSetting.value.isdigit() or (aSetting.value == 'true' or aSetting.value == 'false')):
|
||||
xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue","params":{"setting":"' + aSetting.json_name() + '","value":' + aSetting.value + '}}')
|
||||
else:
|
||||
xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue","params":{"setting":"' + aSetting.json_name() + '","value":"' + utils.encode(aSetting.value) + '"}}')
|
||||
|
||||
#make a copy of the guisettings file to make user based restores easier
|
||||
xbmcvfs.copy(self.settingsFile, xbmc.translatePath("special://home/userdata/guisettings.xml.restored"))
|
||||
|
||||
def __parseNodes(self,nodeList):
|
||||
result = []
|
||||
filename = 'kodi_settings.json'
|
||||
systemSettings = None
|
||||
|
||||
for node in nodeList.childNodes:
|
||||
if(node.nodeType == self.doc.ELEMENT_NODE):
|
||||
aSetting = SettingNode(node.nodeName)
|
||||
def __init__(self):
|
||||
# get all of the current Kodi settings
|
||||
json_response = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings","params":{"level":"expert"}}'))
|
||||
|
||||
#detect if there are any element nodes
|
||||
if(len(node.childNodes) > 0):
|
||||
for child_node in node.childNodes:
|
||||
if(child_node.nodeType == self.doc.ELEMENT_NODE):
|
||||
aSetting.hasChildren = True
|
||||
self.systemSettings = json_response['result']['settings']
|
||||
|
||||
if(not aSetting.hasChildren and len(node.childNodes) > 0):
|
||||
aSetting.value = node.firstChild.nodeValue
|
||||
|
||||
if('default' not in node.attributes.keys()):
|
||||
aSetting.isDefault = False
|
||||
|
||||
aSetting.parent = node.parentNode.nodeName
|
||||
|
||||
result.append(aSetting)
|
||||
return result
|
||||
|
||||
|
||||
def _readFile(self,fileLoc):
|
||||
|
||||
if(xbmcvfs.exists(fileLoc)):
|
||||
try:
|
||||
self.doc = minidom.parse(fileLoc)
|
||||
self.settingsFile = fileLoc
|
||||
except ExpatError:
|
||||
utils.log("Can't read " + fileLoc)
|
||||
|
||||
class SettingNode:
|
||||
name = ''
|
||||
value = ''
|
||||
hasChildren = False
|
||||
isDefault = True
|
||||
parent = ''
|
||||
|
||||
def __init__(self,name):
|
||||
self.name = name
|
||||
|
||||
def json_name(self):
|
||||
return self.parent + "." + self.name
|
||||
|
||||
|
||||
def backup(self):
|
||||
utils.log('Backing up Kodi settings')
|
||||
|
||||
# return all current settings
|
||||
return self.systemSettings
|
||||
|
||||
def restore(self, restoreSettings):
|
||||
utils.log('Restoring Kodi settings')
|
||||
|
||||
updateJson = {"jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": {"setting": "", "value": ""}}
|
||||
|
||||
# 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']
|
||||
|
||||
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'])))
|
||||
|
||||
updateJson['params']['setting'] = aSetting['id']
|
||||
updateJson['params']['value'] = aSetting['value']
|
||||
|
||||
xbmc.executeJSONRPC(json.dumps(updateJson))
|
||||
restoreCount = restoreCount + 1
|
||||
|
||||
utils.log('Update %d settings' % restoreCount)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import utils as utils
|
||||
import xbmcgui
|
||||
from . import utils as utils
|
||||
|
||||
|
||||
class BackupProgressBar:
|
||||
NONE = 2
|
||||
@@ -9,34 +10,34 @@ class BackupProgressBar:
|
||||
mode = 2
|
||||
progressBar = None
|
||||
override = False
|
||||
|
||||
def __init__(self,progressOverride):
|
||||
|
||||
def __init__(self, progressOverride):
|
||||
self.override = progressOverride
|
||||
|
||||
#check if we should use the progress bar
|
||||
if(int(utils.getSetting('progress_mode')) != 2):
|
||||
#check if background or normal
|
||||
if(int(utils.getSetting('progress_mode')) == 0 and not self.override):
|
||||
|
||||
# check if we should use the progress bar
|
||||
if(utils.getSettingInt('progress_mode') != 2):
|
||||
# check if background or normal
|
||||
if(utils.getSettingInt('progress_mode') == 0 and not self.override):
|
||||
self.mode = self.DIALOG
|
||||
self.progressBar = xbmcgui.DialogProgress()
|
||||
else:
|
||||
self.mode = self.BACKGROUND
|
||||
self.progressBar = xbmcgui.DialogProgressBG()
|
||||
|
||||
def create(self,heading,message):
|
||||
def create(self, heading, message):
|
||||
if(self.mode != self.NONE):
|
||||
self.progressBar.create(heading,message)
|
||||
self.progressBar.create(heading, message)
|
||||
|
||||
def updateProgress(self,percent,message=None):
|
||||
|
||||
#update the progress bar
|
||||
def updateProgress(self, percent, message=None):
|
||||
|
||||
# update the progress bar
|
||||
if(self.mode != self.NONE):
|
||||
if(message != None):
|
||||
#need different calls for dialog and background bars
|
||||
if(message is not None):
|
||||
# need different calls for dialog and background bars
|
||||
if(self.mode == self.DIALOG):
|
||||
self.progressBar.update(percent,message)
|
||||
self.progressBar.update(percent, message)
|
||||
else:
|
||||
self.progressBar.update(percent,message=message)
|
||||
self.progressBar.update(percent, message=message)
|
||||
else:
|
||||
self.progressBar.update(percent)
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
Copyright 2013 Google Inc. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
@@ -1,174 +0,0 @@
|
||||
class ApiAttribute(object):
|
||||
"""A data descriptor that sets and returns values."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Create an instance of ApiAttribute.
|
||||
|
||||
:param name: name of this attribute.
|
||||
:type name: str.
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
"""Accesses value of this attribute."""
|
||||
return obj.attr.get(self.name)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
"""Write value of this attribute."""
|
||||
obj.attr[self.name] = value
|
||||
if obj.dirty.get(self.name) is not None:
|
||||
obj.dirty[self.name] = True
|
||||
|
||||
def __del__(self, obj=None):
|
||||
"""Delete value of this attribute."""
|
||||
if(obj != None):
|
||||
del obj.attr[self.name]
|
||||
if obj.dirty.get(self.name) is not None:
|
||||
del obj.dirty[self.name]
|
||||
|
||||
|
||||
class ApiAttributeMixin(object):
|
||||
"""Mixin to initialize required global variables to use ApiAttribute."""
|
||||
|
||||
def __init__(self):
|
||||
self.attr = {}
|
||||
self.dirty = {}
|
||||
|
||||
|
||||
class ApiResource(dict):
|
||||
"""Super class of all api resources.
|
||||
|
||||
Inherits and behaves as a python dictionary to handle api resources.
|
||||
Save clean copy of metadata in self.metadata as a dictionary.
|
||||
Provides changed metadata elements to efficiently update api resources.
|
||||
"""
|
||||
auth = ApiAttribute('auth')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create an instance of ApiResource."""
|
||||
self.update(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Overwritten method of dictionary.
|
||||
|
||||
:param key: key of the query.
|
||||
:type key: str.
|
||||
:returns: value of the query.
|
||||
"""
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
"""Overwritten method of dictionary.
|
||||
|
||||
:param key: key of the query.
|
||||
:type key: str.
|
||||
:param val: value of the query.
|
||||
"""
|
||||
dict.__setitem__(self, key, val)
|
||||
|
||||
def __repr__(self):
|
||||
"""Overwritten method of dictionary."""
|
||||
dictrepr = dict.__repr__(self)
|
||||
return '%s(%s)' % (type(self).__name__, dictrepr)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
"""Overwritten method of dictionary."""
|
||||
for k, v in dict(*args, **kwargs).iteritems():
|
||||
self[k] = v
|
||||
|
||||
def UpdateMetadata(self, metadata=None):
|
||||
"""Update metadata and mark all of them to be clean."""
|
||||
if metadata:
|
||||
self.update(metadata)
|
||||
self.metadata = dict(self)
|
||||
|
||||
def GetChanges(self):
|
||||
"""Returns changed metadata elements to update api resources efficiently.
|
||||
|
||||
:returns: dict -- changed metadata elements.
|
||||
"""
|
||||
dirty = {}
|
||||
for key in self:
|
||||
if self.metadata.get(key) is None:
|
||||
dirty[key] = self[key]
|
||||
elif self.metadata[key] != self[key]:
|
||||
dirty[key] = self[key]
|
||||
return dirty
|
||||
|
||||
|
||||
class ApiResourceList(ApiAttributeMixin, ApiResource):
|
||||
"""Abstract class of all api list resources.
|
||||
|
||||
Inherits ApiResource and builds iterator to list any API resource.
|
||||
"""
|
||||
metadata = ApiAttribute('metadata')
|
||||
|
||||
def __init__(self, auth=None, metadata=None):
|
||||
"""Create an instance of ApiResourceList.
|
||||
|
||||
:param auth: authorized GoogleAuth instance.
|
||||
:type auth: GoogleAuth.
|
||||
:param metadata: parameter to send to list command.
|
||||
:type metadata: dict.
|
||||
"""
|
||||
ApiAttributeMixin.__init__(self)
|
||||
ApiResource.__init__(self)
|
||||
self.auth = auth
|
||||
self.UpdateMetadata()
|
||||
if metadata:
|
||||
self.update(metadata)
|
||||
|
||||
def __iter__(self):
|
||||
"""Returns iterator object.
|
||||
|
||||
:returns: ApiResourceList -- self
|
||||
"""
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
"""Make API call to list resources and return them.
|
||||
|
||||
Auto updates 'pageToken' everytime it makes API call and
|
||||
raises StopIteration when it reached the end of iteration.
|
||||
|
||||
:returns: list -- list of API resources.
|
||||
:raises: StopIteration
|
||||
"""
|
||||
if 'pageToken' in self and self['pageToken'] is None:
|
||||
raise StopIteration
|
||||
result = self._GetList()
|
||||
self['pageToken'] = self.metadata.get('nextPageToken')
|
||||
return result
|
||||
|
||||
def GetList(self):
|
||||
"""Get list of API resources.
|
||||
|
||||
If 'maxResults' is not specified, it will automatically iterate through
|
||||
every resources available. Otherwise, it will make API call once and
|
||||
update 'pageToken'.
|
||||
|
||||
:returns: list -- list of API resources.
|
||||
"""
|
||||
if self.get('maxResults') is None:
|
||||
self['maxResults'] = 1000
|
||||
result = []
|
||||
for x in self:
|
||||
result.extend(x)
|
||||
del self['maxResults']
|
||||
return result
|
||||
else:
|
||||
return self.next()
|
||||
|
||||
def _GetList(self):
|
||||
"""Helper function which actually makes API call.
|
||||
|
||||
Should be overwritten.
|
||||
|
||||
:raises: NotImplementedError
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def Reset(self):
|
||||
"""Resets current iteration"""
|
||||
if 'pageToken' in self:
|
||||
del self['pageToken']
|
||||
@@ -1,415 +0,0 @@
|
||||
import socket
|
||||
import webbrowser
|
||||
import httplib2
|
||||
import oauth2client.clientsecrets as clientsecrets
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
from functools import wraps
|
||||
from oauth2client.client import FlowExchangeError
|
||||
from oauth2client.client import AccessTokenRefreshError
|
||||
from oauth2client.client import OAuth2WebServerFlow
|
||||
from oauth2client.client import OOB_CALLBACK_URN
|
||||
from oauth2client.file import Storage
|
||||
from oauth2client.tools import ClientRedirectHandler
|
||||
from oauth2client.tools import ClientRedirectServer
|
||||
from oauth2client._helpers import scopes_to_string
|
||||
from .apiattr import ApiAttribute
|
||||
from .apiattr import ApiAttributeMixin
|
||||
from .settings import LoadSettingsFile
|
||||
from .settings import ValidateSettings
|
||||
from .settings import SettingsError
|
||||
from .settings import InvalidConfigError
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
"""Base error for authentication/authorization errors."""
|
||||
|
||||
|
||||
class InvalidCredentialsError(IOError):
|
||||
"""Error trying to read credentials file."""
|
||||
|
||||
|
||||
class AuthenticationRejected(AuthError):
|
||||
"""User rejected authentication."""
|
||||
|
||||
|
||||
class AuthenticationError(AuthError):
|
||||
"""General authentication error."""
|
||||
|
||||
|
||||
class RefreshError(AuthError):
|
||||
"""Access token refresh error."""
|
||||
|
||||
def LoadAuth(decoratee):
|
||||
"""Decorator to check if the auth is valid and loads auth if not."""
|
||||
@wraps(decoratee)
|
||||
def _decorated(self, *args, **kwargs):
|
||||
if self.auth is None: # Initialize auth if needed.
|
||||
self.auth = GoogleAuth()
|
||||
if self.auth.access_token_expired:
|
||||
self.auth.LocalWebserverAuth()
|
||||
if self.auth.service is None: # Check if drive api is built.
|
||||
self.auth.Authorize()
|
||||
return decoratee(self, *args, **kwargs)
|
||||
return _decorated
|
||||
|
||||
def CheckAuth(decoratee):
|
||||
"""Decorator to check if it requires OAuth2 flow request."""
|
||||
@wraps(decoratee)
|
||||
def _decorated(self, *args, **kwargs):
|
||||
dirty = False
|
||||
code = None
|
||||
save_credentials = self.settings.get('save_credentials')
|
||||
if self.credentials is None and save_credentials:
|
||||
self.LoadCredentials()
|
||||
if self.flow is None:
|
||||
self.GetFlow()
|
||||
if self.credentials is None:
|
||||
code = decoratee(self, *args, **kwargs)
|
||||
dirty = True
|
||||
else:
|
||||
if self.access_token_expired:
|
||||
if self.credentials.refresh_token is not None:
|
||||
self.Refresh()
|
||||
else:
|
||||
code = decoratee(self, *args, **kwargs)
|
||||
dirty = True
|
||||
if code is not None:
|
||||
self.Auth(code)
|
||||
if dirty and save_credentials:
|
||||
self.SaveCredentials()
|
||||
return _decorated
|
||||
|
||||
|
||||
class GoogleAuth(ApiAttributeMixin, object):
|
||||
"""Wrapper class for oauth2client library in google-api-python-client.
|
||||
|
||||
Loads all settings and credentials from one 'settings.yaml' file
|
||||
and performs common OAuth2.0 related functionality such as authentication
|
||||
and authorization.
|
||||
"""
|
||||
DEFAULT_SETTINGS = {
|
||||
'client_config_backend': 'file',
|
||||
'client_config_file': 'client_secrets.json',
|
||||
'save_credentials': False,
|
||||
'oauth_scope': ['https://www.googleapis.com/auth/drive']
|
||||
}
|
||||
CLIENT_CONFIGS_LIST = ['client_id', 'client_secret', 'auth_uri',
|
||||
'token_uri', 'revoke_uri', 'redirect_uri']
|
||||
settings = ApiAttribute('settings')
|
||||
client_config = ApiAttribute('client_config')
|
||||
flow = ApiAttribute('flow')
|
||||
credentials = ApiAttribute('credentials')
|
||||
http = ApiAttribute('http')
|
||||
service = ApiAttribute('service')
|
||||
|
||||
def __init__(self, settings_file='settings.yaml'):
|
||||
"""Create an instance of GoogleAuth.
|
||||
|
||||
This constructor just sets the path of settings file.
|
||||
It does not actually read the file.
|
||||
|
||||
:param settings_file: path of settings file. 'settings.yaml' by default.
|
||||
:type settings_file: str.
|
||||
"""
|
||||
ApiAttributeMixin.__init__(self)
|
||||
self.client_config = {}
|
||||
try:
|
||||
self.settings = LoadSettingsFile(settings_file)
|
||||
except SettingsError:
|
||||
self.settings = self.DEFAULT_SETTINGS
|
||||
else:
|
||||
if self.settings is None:
|
||||
self.settings = self.DEFAULT_SETTINGS
|
||||
else:
|
||||
ValidateSettings(self.settings)
|
||||
|
||||
@property
|
||||
def access_token_expired(self):
|
||||
"""Checks if access token doesn't exist or is expired.
|
||||
|
||||
:returns: bool -- True if access token doesn't exist or is expired.
|
||||
"""
|
||||
if self.credentials is None:
|
||||
return True
|
||||
return self.credentials.access_token_expired
|
||||
|
||||
@CheckAuth
|
||||
def LocalWebserverAuth(self, host_name='localhost',
|
||||
port_numbers=[8080, 8090]):
|
||||
"""Authenticate and authorize from user by creating local webserver and
|
||||
retrieving authentication code.
|
||||
|
||||
This function is not for webserver application. It creates local webserver
|
||||
for user from standalone application.
|
||||
|
||||
:param host_name: host name of the local webserver.
|
||||
:type host_name: str.
|
||||
:param port_numbers: list of port numbers to be tried to used.
|
||||
:type port_numbers: list.
|
||||
:returns: str -- code returned from local webserver
|
||||
:raises: AuthenticationRejected, AuthenticationError
|
||||
"""
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in port_numbers:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((host_name, port), ClientRedirectHandler)
|
||||
except socket.error as e:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
if success:
|
||||
oauth_callback = 'http://%s:%s/' % (host_name, port_number)
|
||||
else:
|
||||
raise AuthenticationError()
|
||||
self.flow.redirect_uri = oauth_callback
|
||||
authorize_url = self.GetAuthUrl()
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
raise AuthenticationRejected('User rejected authentication')
|
||||
if 'code' in httpd.query_params:
|
||||
return httpd.query_params['code']
|
||||
else:
|
||||
raise AuthenticationError('No code found in redirect')
|
||||
|
||||
@CheckAuth
|
||||
def CommandLineAuth(self):
|
||||
"""Authenticate and authorize from user by printing authentication url
|
||||
retrieving authentication code from command-line.
|
||||
|
||||
:returns: str -- code returned from commandline.
|
||||
"""
|
||||
self.flow.redirect_uri = OOB_CALLBACK_URN
|
||||
authorize_url = self.GetAuthUrl()
|
||||
return raw_input('Enter verification code: ').strip()
|
||||
|
||||
def LoadCredentials(self, backend=None):
|
||||
"""Loads credentials or create empty credentials if it doesn't exist.
|
||||
|
||||
:param backend: target backend to save credential to.
|
||||
:type backend: str.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
if backend is None:
|
||||
backend = self.settings.get('save_credentials_backend')
|
||||
if backend is None:
|
||||
raise InvalidConfigError('Please specify credential backend')
|
||||
if backend == 'file':
|
||||
self.LoadCredentialsFile()
|
||||
else:
|
||||
raise InvalidConfigError('Unknown save_credentials_backend')
|
||||
|
||||
def LoadCredentialsFile(self, credentials_file=None):
|
||||
"""Loads credentials or create empty credentials if it doesn't exist.
|
||||
|
||||
Loads credentials file from path in settings if not specified.
|
||||
|
||||
:param credentials_file: path of credentials file to read.
|
||||
:type credentials_file: str.
|
||||
:raises: InvalidConfigError, InvalidCredentialsError
|
||||
"""
|
||||
if credentials_file is None:
|
||||
credentials_file = self.settings.get('save_credentials_file')
|
||||
if credentials_file is None:
|
||||
raise InvalidConfigError('Please specify credentials file to read')
|
||||
try:
|
||||
storage = Storage(credentials_file)
|
||||
self.credentials = storage.get()
|
||||
except IOError:
|
||||
raise InvalidCredentialsError('Credentials file cannot be symbolic link')
|
||||
|
||||
def SaveCredentials(self, backend=None):
|
||||
"""Saves credentials according to specified backend.
|
||||
|
||||
If you have any specific credentials backend in mind, don't use this
|
||||
function and use the corresponding function you want.
|
||||
|
||||
:param backend: backend to save credentials.
|
||||
:type backend: str.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
if backend is None:
|
||||
backend = self.settings.get('save_credentials_backend')
|
||||
if backend is None:
|
||||
raise InvalidConfigError('Please specify credential backend')
|
||||
if backend == 'file':
|
||||
self.SaveCredentialsFile()
|
||||
else:
|
||||
raise InvalidConfigError('Unknown save_credentials_backend')
|
||||
|
||||
def SaveCredentialsFile(self, credentials_file=None):
|
||||
"""Saves credentials to the file in JSON format.
|
||||
|
||||
:param credentials_file: destination to save file to.
|
||||
:type credentials_file: str.
|
||||
:raises: InvalidConfigError, InvalidCredentialsError
|
||||
"""
|
||||
if self.credentials is None:
|
||||
raise InvalidCredentialsError('No credentials to save')
|
||||
if credentials_file is None:
|
||||
credentials_file = self.settings.get('save_credentials_file')
|
||||
if credentials_file is None:
|
||||
raise InvalidConfigError('Please specify credentials file to read')
|
||||
try:
|
||||
storage = Storage(credentials_file)
|
||||
storage.put(self.credentials)
|
||||
self.credentials.set_store(storage)
|
||||
except CredentialsFileSymbolicLinkError:
|
||||
raise InvalidCredentialsError('Credentials file cannot be symbolic link')
|
||||
|
||||
def LoadClientConfig(self, backend=None):
|
||||
"""Loads client configuration according to specified backend.
|
||||
|
||||
If you have any specific backend to load client configuration from in mind,
|
||||
don't use this function and use the corresponding function you want.
|
||||
|
||||
:param backend: backend to load client configuration from.
|
||||
:type backend: str.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
if backend is None:
|
||||
backend = self.settings.get('client_config_backend')
|
||||
if backend is None:
|
||||
raise InvalidConfigError('Please specify client config backend')
|
||||
if backend == 'file':
|
||||
self.LoadClientConfigFile()
|
||||
elif backend == 'settings':
|
||||
self.LoadClientConfigSettings()
|
||||
else:
|
||||
raise InvalidConfigError('Unknown client_config_backend')
|
||||
|
||||
def LoadClientConfigFile(self, client_config_file=None):
|
||||
"""Loads client configuration file downloaded from APIs console.
|
||||
|
||||
Loads client config file from path in settings if not specified.
|
||||
|
||||
:param client_config_file: path of client config file to read.
|
||||
:type client_config_file: str.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
if client_config_file is None:
|
||||
client_config_file = self.settings['client_config_file']
|
||||
try:
|
||||
client_type, client_info = clientsecrets.loadfile(client_config_file)
|
||||
except clientsecrets.InvalidClientSecretsError as error:
|
||||
raise InvalidConfigError('Invalid client secrets file %s' % error)
|
||||
if not client_type in (clientsecrets.TYPE_WEB,
|
||||
clientsecrets.TYPE_INSTALLED):
|
||||
raise InvalidConfigError('Unknown client_type of client config file')
|
||||
try:
|
||||
config_index = ['client_id', 'client_secret', 'auth_uri', 'token_uri']
|
||||
for config in config_index:
|
||||
self.client_config[config] = client_info[config]
|
||||
self.client_config['revoke_uri'] = client_info.get('revoke_uri')
|
||||
self.client_config['redirect_uri'] = client_info['redirect_uris'][0]
|
||||
except KeyError:
|
||||
raise InvalidConfigError('Insufficient client config in file')
|
||||
|
||||
def LoadClientConfigSettings(self):
|
||||
"""Loads client configuration from settings file.
|
||||
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
|
||||
for config in self.CLIENT_CONFIGS_LIST:
|
||||
try:
|
||||
self.client_config[config] = self.settings['client_config'][config]
|
||||
|
||||
except KeyError:
|
||||
raise InvalidConfigError('Insufficient client config in settings')
|
||||
|
||||
def GetFlow(self):
|
||||
"""Gets Flow object from client configuration.
|
||||
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
if not all(config in self.client_config \
|
||||
for config in self.CLIENT_CONFIGS_LIST):
|
||||
self.LoadClientConfig()
|
||||
constructor_kwargs = {
|
||||
'redirect_uri': self.client_config['redirect_uri'],
|
||||
'auth_uri': self.client_config['auth_uri'],
|
||||
'token_uri': self.client_config['token_uri'],
|
||||
}
|
||||
if self.client_config['revoke_uri'] is not None:
|
||||
constructor_kwargs['revoke_uri'] = self.client_config['revoke_uri']
|
||||
self.flow = OAuth2WebServerFlow(
|
||||
self.client_config['client_id'],
|
||||
self.client_config['client_secret'],
|
||||
scopes_to_string(self.settings['oauth_scope']),
|
||||
**constructor_kwargs)
|
||||
if self.settings.get('get_refresh_token'):
|
||||
self.flow.params.update({'access_type': 'offline'})
|
||||
|
||||
def Refresh(self):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
:raises: RefreshError
|
||||
"""
|
||||
if self.credentials is None:
|
||||
raise RefreshError('No credential to refresh.')
|
||||
if self.credentials.refresh_token is None:
|
||||
raise RefreshError('No refresh_token found.'
|
||||
'Please set access_type of OAuth to offline.')
|
||||
if self.http is None:
|
||||
self.http = httplib2.Http()
|
||||
try:
|
||||
self.credentials.refresh(self.http)
|
||||
except AccessTokenRefreshError as error:
|
||||
raise RefreshError('Access token refresh failed: %s' % error)
|
||||
|
||||
def GetAuthUrl(self, keys = None):
|
||||
"""Creates authentication url where user visits to grant access.
|
||||
|
||||
:returns: str -- Authentication url.
|
||||
"""
|
||||
|
||||
if(keys != None):
|
||||
#update some of the settings in the client_config dict
|
||||
self.client_config['client_id'] = keys['client_id']
|
||||
self.client_config['client_secret'] = keys['client_secret']
|
||||
|
||||
if self.flow is None:
|
||||
self.GetFlow()
|
||||
|
||||
return self.flow.step1_get_authorize_url()
|
||||
|
||||
def Auth(self, code):
|
||||
"""Authenticate, authorize, and build service.
|
||||
|
||||
:param code: Code for authentication.
|
||||
:type code: str.
|
||||
:raises: AuthenticationError
|
||||
"""
|
||||
self.Authenticate(code)
|
||||
self.Authorize()
|
||||
|
||||
def Authenticate(self, code):
|
||||
"""Authenticates given authentication code back from user.
|
||||
|
||||
:param code: Code for authentication.
|
||||
:type code: str.
|
||||
:raises: AuthenticationError
|
||||
"""
|
||||
if self.flow is None:
|
||||
self.GetFlow()
|
||||
try:
|
||||
self.credentials = self.flow.step2_exchange(code)
|
||||
except FlowExchangeError as e:
|
||||
raise AuthenticationError('OAuth2 code exchange failed: %s' % e)
|
||||
|
||||
def Authorize(self):
|
||||
"""Authorizes and builds service.
|
||||
|
||||
:raises: AuthenticationError
|
||||
"""
|
||||
if self.http is None:
|
||||
self.http = httplib2.Http()
|
||||
if self.access_token_expired:
|
||||
raise AuthenticationError('No valid credentials provided to authorize')
|
||||
self.http = self.credentials.authorize(self.http)
|
||||
self.service = build('drive', 'v2', http=self.http)
|
||||
@@ -1,38 +0,0 @@
|
||||
from .apiattr import ApiAttributeMixin
|
||||
from .files import GoogleDriveFile
|
||||
from .files import GoogleDriveFileList
|
||||
|
||||
|
||||
class GoogleDrive(ApiAttributeMixin, object):
|
||||
"""Main Google Drive class."""
|
||||
|
||||
def __init__(self, auth=None):
|
||||
"""Create an instance of GoogleDrive.
|
||||
|
||||
:param auth: authorized GoogleAuth instance.
|
||||
:type auth: pydrive.auth.GoogleAuth.
|
||||
"""
|
||||
ApiAttributeMixin.__init__(self)
|
||||
self.auth = auth
|
||||
|
||||
def CreateFile(self, metadata=None):
|
||||
"""Create an instance of GoogleDriveFile with auth of this instance.
|
||||
|
||||
This method would not upload a file to GoogleDrive.
|
||||
|
||||
:param metadata: file resource to initialize GoogleDriveFile with.
|
||||
:type metadata: dict.
|
||||
:returns: pydrive.files.GoogleDriveFile -- initialized with auth of this instance.
|
||||
"""
|
||||
return GoogleDriveFile(auth=self.auth, metadata=metadata)
|
||||
|
||||
def ListFile(self, param=None):
|
||||
"""Create an instance of GoogleDriveFileList with auth of this instance.
|
||||
|
||||
This method will not fetch from Files.List().
|
||||
|
||||
:param param: parameter to be sent to Files.List().
|
||||
:type param: dict.
|
||||
:returns: pydrive.files.GoogleDriveFileList -- initialized with auth of this instance.
|
||||
"""
|
||||
return GoogleDriveFileList(auth=self.auth, param=param)
|
||||
@@ -1,322 +0,0 @@
|
||||
import io
|
||||
import mimetypes
|
||||
|
||||
from googleapiclient import errors
|
||||
from googleapiclient.http import MediaIoBaseUpload
|
||||
from functools import wraps
|
||||
|
||||
from .apiattr import ApiAttribute
|
||||
from .apiattr import ApiAttributeMixin
|
||||
from .apiattr import ApiResource
|
||||
from .apiattr import ApiResourceList
|
||||
from .auth import LoadAuth
|
||||
|
||||
|
||||
class FileNotUploadedError(RuntimeError):
|
||||
"""Error trying to access metadata of file that is not uploaded."""
|
||||
|
||||
|
||||
class ApiRequestError(IOError):
|
||||
"""Error while making any API requests."""
|
||||
|
||||
|
||||
class FileNotDownloadableError(RuntimeError):
|
||||
"""Error trying to download file that is not downloadable."""
|
||||
|
||||
|
||||
def LoadMetadata(decoratee):
|
||||
"""Decorator to check if the file has metadata and fetches it if not.
|
||||
|
||||
:raises: ApiRequestError, FileNotUploadedError
|
||||
"""
|
||||
@wraps(decoratee)
|
||||
def _decorated(self, *args, **kwargs):
|
||||
if not self.uploaded:
|
||||
self.FetchMetadata()
|
||||
return decoratee(self, *args, **kwargs)
|
||||
return _decorated
|
||||
|
||||
|
||||
class GoogleDriveFileList(ApiResourceList):
|
||||
"""Google Drive FileList instance.
|
||||
|
||||
Equivalent to Files.list() in Drive APIs.
|
||||
"""
|
||||
|
||||
def __init__(self, auth=None, param=None):
|
||||
"""Create an instance of GoogleDriveFileList."""
|
||||
super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param)
|
||||
|
||||
@LoadAuth
|
||||
def _GetList(self):
|
||||
"""Overwritten method which actually makes API call to list files.
|
||||
|
||||
:returns: list -- list of pydrive.files.GoogleDriveFile.
|
||||
"""
|
||||
self.metadata = self.auth.service.files().list(**dict(self)).execute()
|
||||
result = []
|
||||
for file_metadata in self.metadata['items']:
|
||||
tmp_file = GoogleDriveFile(
|
||||
auth=self.auth,
|
||||
metadata=file_metadata,
|
||||
uploaded=True)
|
||||
result.append(tmp_file)
|
||||
return result
|
||||
|
||||
|
||||
class GoogleDriveFile(ApiAttributeMixin, ApiResource):
|
||||
"""Google Drive File instance.
|
||||
|
||||
Inherits ApiResource which inherits dict.
|
||||
Can access and modify metadata like dictionary.
|
||||
"""
|
||||
content = ApiAttribute('content')
|
||||
uploaded = ApiAttribute('uploaded')
|
||||
metadata = ApiAttribute('metadata')
|
||||
|
||||
def __init__(self, auth=None, metadata=None, uploaded=False):
|
||||
"""Create an instance of GoogleDriveFile.
|
||||
|
||||
:param auth: authorized GoogleAuth instance.
|
||||
:type auth: pydrive.auth.GoogleAuth
|
||||
:param metadata: file resource to initialize GoogleDirveFile with.
|
||||
:type metadata: dict.
|
||||
:param uploaded: True if this file is confirmed to be uploaded.
|
||||
:type uploaded: bool.
|
||||
"""
|
||||
ApiAttributeMixin.__init__(self)
|
||||
ApiResource.__init__(self)
|
||||
self.metadata = {}
|
||||
self.dirty = {'content': False}
|
||||
self.auth = auth
|
||||
self.uploaded = uploaded
|
||||
if uploaded:
|
||||
self.UpdateMetadata(metadata)
|
||||
elif metadata:
|
||||
self.update(metadata)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Overwrites manner of accessing Files resource.
|
||||
|
||||
If this file instance is not uploaded and id is specified,
|
||||
it will try to look for metadata with Files.get().
|
||||
|
||||
:param key: key of dictionary query.
|
||||
:type key: str.
|
||||
:returns: value of Files resource
|
||||
:raises: KeyError, FileNotUploadedError
|
||||
"""
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError as e:
|
||||
if self.uploaded:
|
||||
raise KeyError(e)
|
||||
if self.get('id'):
|
||||
self.FetchMetadata()
|
||||
return dict.__getitem__(self, key)
|
||||
else:
|
||||
raise FileNotUploadedError()
|
||||
|
||||
def SetContentString(self, content):
|
||||
"""Set content of this file to be a string.
|
||||
|
||||
Creates io.BytesIO instance of utf-8 encoded string.
|
||||
Sets mimeType to be 'text/plain' if not specified.
|
||||
|
||||
:param content: content of the file in string.
|
||||
:type content: str.
|
||||
"""
|
||||
self.content = io.BytesIO(content.encode('utf-8'))
|
||||
if self.get('mimeType') is None:
|
||||
self['mimeType'] = 'text/plain'
|
||||
|
||||
def SetContentFile(self, filename):
|
||||
"""Set content of this file from a file.
|
||||
|
||||
Opens the file specified by this method.
|
||||
Will be read, uploaded, and closed by Upload() method.
|
||||
Sets metadata 'title' and 'mimeType' automatically if not specified.
|
||||
|
||||
:param filename: name of the file to be uploaded.
|
||||
:type filename: str.
|
||||
"""
|
||||
self.content = open(filename, 'rb')
|
||||
|
||||
if self.get('title') is None:
|
||||
self['title'] = filename
|
||||
if self.get('mimeType') is None:
|
||||
self['mimeType'] = mimetypes.guess_type(filename)[0]
|
||||
|
||||
def GetContentString(self):
|
||||
"""Get content of this file as a string.
|
||||
|
||||
:returns: str -- utf-8 decoded content of the file
|
||||
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
||||
"""
|
||||
if self.content is None or type(self.content) is not io.BytesIO:
|
||||
self.FetchContent()
|
||||
return self.content.getvalue().decode('utf-8')
|
||||
|
||||
def GetContentFile(self, filename, mimetype=None):
|
||||
"""Save content of this file as a local file.
|
||||
|
||||
:param filename: name of the file to write to.
|
||||
:type filename: str.
|
||||
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
||||
"""
|
||||
if self.content is None or type(self.content) is not io.BytesIO:
|
||||
self.FetchContent(mimetype)
|
||||
f = open(filename, 'wb')
|
||||
f.write(self.content.getvalue())
|
||||
f.close()
|
||||
|
||||
@LoadAuth
|
||||
def FetchMetadata(self):
|
||||
"""Download file's metadata from id using Files.get().
|
||||
|
||||
:raises: ApiRequestError, FileNotUploadedError
|
||||
"""
|
||||
file_id = self.metadata.get('id') or self.get('id')
|
||||
if file_id:
|
||||
try:
|
||||
metadata = self.auth.service.files().get(fileId=file_id).execute()
|
||||
except errors.HttpError as error:
|
||||
raise ApiRequestError(error)
|
||||
else:
|
||||
self.uploaded = True
|
||||
self.UpdateMetadata(metadata)
|
||||
else:
|
||||
raise FileNotUploadedError()
|
||||
|
||||
@LoadMetadata
|
||||
def FetchContent(self, mimetype=None):
|
||||
"""Download file's content from download_url.
|
||||
|
||||
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
|
||||
"""
|
||||
download_url = self.metadata.get('downloadUrl')
|
||||
if download_url:
|
||||
self.content = io.BytesIO(self._DownloadFromUrl(download_url))
|
||||
self.dirty['content'] = False
|
||||
return
|
||||
|
||||
export_links = self.metadata.get('exportLinks')
|
||||
if export_links and export_links.get(mimetype):
|
||||
self.content = io.BytesIO(
|
||||
self._DownloadFromUrl(export_links.get(mimetype)))
|
||||
self.dirty['content'] = False
|
||||
return
|
||||
|
||||
raise FileNotDownloadableError(
|
||||
'No downloadLink/exportLinks for mimetype found in metadata')
|
||||
|
||||
def Upload(self, param=None):
|
||||
"""Upload/update file by choosing the most efficient method.
|
||||
|
||||
:param param: additional parameter to upload file.
|
||||
:type param: dict.
|
||||
:raises: ApiRequestError
|
||||
"""
|
||||
if self.uploaded or self.get('id') is not None:
|
||||
if self.dirty['content']:
|
||||
self._FilesUpdate(param=param)
|
||||
else:
|
||||
self._FilesPatch(param=param)
|
||||
else:
|
||||
self._FilesInsert(param=param)
|
||||
|
||||
def Delete(self):
|
||||
if self.get('id') is not None:
|
||||
self.auth.service.files().delete(fileId=self.get('id')).execute()
|
||||
|
||||
@LoadAuth
|
||||
def _FilesInsert(self, param=None):
|
||||
"""Upload a new file using Files.insert().
|
||||
|
||||
:param param: additional parameter to upload file.
|
||||
:type param: dict.
|
||||
:raises: ApiRequestError
|
||||
"""
|
||||
if param is None:
|
||||
param = {}
|
||||
param['body'] = self.GetChanges()
|
||||
try:
|
||||
if self.dirty['content']:
|
||||
param['media_body'] = self._BuildMediaBody()
|
||||
metadata = self.auth.service.files().insert(**param).execute()
|
||||
except errors.HttpError as error:
|
||||
raise ApiRequestError(error)
|
||||
else:
|
||||
self.uploaded = True
|
||||
self.dirty['content'] = False
|
||||
self.UpdateMetadata(metadata)
|
||||
|
||||
@LoadAuth
|
||||
@LoadMetadata
|
||||
def _FilesUpdate(self, param=None):
|
||||
"""Update metadata and/or content using Files.Update().
|
||||
|
||||
:param param: additional parameter to upload file.
|
||||
:type param: dict.
|
||||
:raises: ApiRequestError, FileNotUploadedError
|
||||
"""
|
||||
if param is None:
|
||||
param = {}
|
||||
param['body'] = self.GetChanges()
|
||||
param['fileId'] = self.metadata.get('id')
|
||||
try:
|
||||
if self.dirty['content']:
|
||||
param['media_body'] = self._BuildMediaBody()
|
||||
metadata = self.auth.service.files().update(**param).execute()
|
||||
except errors.HttpError as error:
|
||||
raise ApiRequestError(error)
|
||||
else:
|
||||
self.uploaded = True
|
||||
self.dirty['content'] = False
|
||||
self.UpdateMetadata(metadata)
|
||||
|
||||
@LoadAuth
|
||||
@LoadMetadata
|
||||
def _FilesPatch(self, param=None):
|
||||
"""Update metadata using Files.Patch().
|
||||
|
||||
:param param: additional parameter to upload file.
|
||||
:type param: dict.
|
||||
:raises: ApiRequestError, FileNotUploadedError
|
||||
"""
|
||||
if param is None:
|
||||
param = {}
|
||||
param['body'] = self.GetChanges()
|
||||
param['fileId'] = self.metadata.get('id')
|
||||
try:
|
||||
metadata = self.auth.service.files().patch(**param).execute()
|
||||
except errors.HttpError as error:
|
||||
raise ApiRequestError(error)
|
||||
else:
|
||||
self.UpdateMetadata(metadata)
|
||||
|
||||
def _BuildMediaBody(self):
|
||||
"""Build MediaIoBaseUpload to get prepared to upload content of the file.
|
||||
|
||||
Sets mimeType as 'application/octet-stream' if not specified.
|
||||
|
||||
:returns: MediaIoBaseUpload -- instance that will be used to upload content.
|
||||
"""
|
||||
if self.get('mimeType') is None:
|
||||
self['mimeType'] = 'application/octet-stream'
|
||||
|
||||
return MediaIoBaseUpload(self.content, self['mimeType'])
|
||||
|
||||
@LoadAuth
|
||||
def _DownloadFromUrl(self, url):
|
||||
"""Download file from url using provided credential.
|
||||
|
||||
:param url: link of the file to download.
|
||||
:type url: str.
|
||||
:returns: str -- content of downloaded file in string.
|
||||
:raises: ApiRequestError
|
||||
"""
|
||||
resp, content = self.auth.service._http.request(url)
|
||||
if resp.status != 200:
|
||||
raise ApiRequestError('Cannot download file: %s' % resp)
|
||||
return content
|
||||
@@ -1,192 +0,0 @@
|
||||
from yaml import load
|
||||
from yaml import YAMLError
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
SETTINGS_FILE = 'settings.yaml'
|
||||
SETTINGS_STRUCT = {
|
||||
'client_config_backend': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default': 'file',
|
||||
'dependency': [
|
||||
{
|
||||
'value': 'file',
|
||||
'attribute': ['client_config_file']
|
||||
},
|
||||
{
|
||||
'value': 'settings',
|
||||
'attribute': ['client_config']
|
||||
}
|
||||
]
|
||||
},
|
||||
'save_credentials': {
|
||||
'type': bool,
|
||||
'required': True,
|
||||
'default': False,
|
||||
'dependency': [
|
||||
{
|
||||
'value': True,
|
||||
'attribute': ['save_credentials_backend']
|
||||
}
|
||||
]
|
||||
},
|
||||
'get_refresh_token': {
|
||||
'type': bool,
|
||||
'required': False,
|
||||
'default': False
|
||||
},
|
||||
'client_config_file': {
|
||||
'type': str,
|
||||
'required': False,
|
||||
'default': 'client_secrets.json'
|
||||
},
|
||||
'save_credentials_backend': {
|
||||
'type': str,
|
||||
'required': False,
|
||||
'dependency': [
|
||||
{
|
||||
'value': 'file',
|
||||
'attribute': ['save_credentials_file']
|
||||
}
|
||||
]
|
||||
},
|
||||
'client_config': {
|
||||
'type': dict,
|
||||
'required': False,
|
||||
'struct': {
|
||||
'client_id': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default':'blank'
|
||||
},
|
||||
'client_secret': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default':'blank'
|
||||
},
|
||||
'auth_uri': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default': 'https://accounts.google.com/o/oauth2/auth'
|
||||
},
|
||||
'token_uri': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default': 'https://accounts.google.com/o/oauth2/token'
|
||||
},
|
||||
'redirect_uri': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default': 'urn:ietf:wg:oauth:2.0:oob'
|
||||
},
|
||||
'revoke_uri': {
|
||||
'type': str,
|
||||
'required': True,
|
||||
'default': None
|
||||
}
|
||||
}
|
||||
},
|
||||
'oauth_scope': {
|
||||
'type': list,
|
||||
'required': True,
|
||||
'struct': str,
|
||||
'default': ['https://www.googleapis.com/auth/drive']
|
||||
},
|
||||
'save_credentials_file': {
|
||||
'type': str,
|
||||
'required': False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SettingsError(IOError):
|
||||
"""Error while loading/saving settings"""
|
||||
|
||||
|
||||
class InvalidConfigError(IOError):
|
||||
"""Error trying to read client configuration."""
|
||||
|
||||
|
||||
def LoadSettingsFile(filename=SETTINGS_FILE):
|
||||
"""Loads settings file in yaml format given file name.
|
||||
|
||||
:param filename: path for settings file. 'settings.yaml' by default.
|
||||
:type filename: str.
|
||||
:raises: SettingsError
|
||||
"""
|
||||
try:
|
||||
stream = file(filename, 'r')
|
||||
data = load(stream, Loader=Loader)
|
||||
except (YAMLError, IOError) as e:
|
||||
raise SettingsError(e)
|
||||
return data
|
||||
|
||||
|
||||
def ValidateSettings(data):
|
||||
"""Validates if current settings is valid.
|
||||
|
||||
:param data: dictionary containing all settings.
|
||||
:type data: dict.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
_ValidateSettingsStruct(data, SETTINGS_STRUCT)
|
||||
|
||||
|
||||
def _ValidateSettingsStruct(data, struct):
|
||||
"""Validates if provided data fits provided structure.
|
||||
|
||||
:param data: dictionary containing settings.
|
||||
:type data: dict.
|
||||
:param struct: dictionary containing structure information of settings.
|
||||
:type struct: dict.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
# Validate required elements of the setting.
|
||||
for key in struct:
|
||||
if struct[key]['required']:
|
||||
_ValidateSettingsElement(data, struct, key)
|
||||
|
||||
|
||||
def _ValidateSettingsElement(data, struct, key):
|
||||
"""Validates if provided element of settings data fits provided structure.
|
||||
|
||||
:param data: dictionary containing settings.
|
||||
:type data: dict.
|
||||
:param struct: dictionary containing structure information of settings.
|
||||
:type struct: dict.
|
||||
:param key: key of the settings element to validate.
|
||||
:type key: str.
|
||||
:raises: InvalidConfigError
|
||||
"""
|
||||
# Check if data exists. If not, check if default value exists.
|
||||
value = data.get(key)
|
||||
data_type = struct[key]['type']
|
||||
if value is None:
|
||||
try:
|
||||
default = struct[key]['default']
|
||||
except KeyError:
|
||||
raise InvalidConfigError('Missing required setting %s' % key)
|
||||
else:
|
||||
data[key] = default
|
||||
# If data exists, Check type of the data
|
||||
elif type(value) is not data_type:
|
||||
raise InvalidConfigError('Setting %s should be type %s' % (key, data_type))
|
||||
# If type of this data is dict, check if structure of the data is valid.
|
||||
if data_type is dict:
|
||||
_ValidateSettingsStruct(data[key], struct[key]['struct'])
|
||||
# If type of this data is list, check if all values in the list is valid.
|
||||
elif data_type is list:
|
||||
for element in data[key]:
|
||||
if type(element) is not struct[key]['struct']:
|
||||
raise InvalidConfigError('Setting %s should be list of %s' %
|
||||
(key, struct[key]['struct']))
|
||||
# Check dependency of this attribute.
|
||||
dependencies = struct[key].get('dependency')
|
||||
if dependencies:
|
||||
for dependency in dependencies:
|
||||
if value == dependency['value']:
|
||||
for reqkey in dependency['attribute']:
|
||||
_ValidateSettingsElement(data, struct, reqkey)
|
||||
@@ -1,7 +0,0 @@
|
||||
client_config_backend: 'settings'
|
||||
client_config:
|
||||
client_id: "blank"
|
||||
client_secret: "blank"
|
||||
get_refresh_token: True
|
||||
oauth_scope:
|
||||
- "https://www.googleapis.com/auth/drive.file"
|
||||
@@ -1,430 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
class relativedelta:
|
||||
"""
|
||||
The relativedelta type is based on the specification of the excelent
|
||||
work done by M.-A. Lemburg in his mx.DateTime extension. However,
|
||||
notice that this type does *NOT* implement the same algorithm as
|
||||
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||
|
||||
There's two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes:
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
And the other way is to use the following keyword arguments:
|
||||
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information.
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative.
|
||||
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc). These instances may
|
||||
receive a parameter N, specifying the Nth weekday, which could
|
||||
be positive or negative (like MO(+1) or MO(-2). Not specifying
|
||||
it is the same as specifying +1. You can also use an integer,
|
||||
where 0=MO.
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
Here is the behavior of operations with relativedelta:
|
||||
|
||||
1) Calculate the absolute year, using the 'year' argument, or the
|
||||
original datetime year, if the argument is not present.
|
||||
|
||||
2) Add the relative 'years' argument to the absolute year.
|
||||
|
||||
3) Do steps 1 and 2 for month/months.
|
||||
|
||||
4) Calculate the absolute day, using the 'day' argument, or the
|
||||
original datetime day, if the argument is not present. Then,
|
||||
subtract from the day until it fits in the year and month
|
||||
found after their operations.
|
||||
|
||||
5) Add the relative 'days' argument to the absolute day. Notice
|
||||
that the 'weeks' argument is multiplied by 7 and added to
|
||||
'days'.
|
||||
|
||||
6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
|
||||
microsecond/microseconds.
|
||||
|
||||
7) If the 'weekday' argument is present, calculate the weekday,
|
||||
with the given (wday, nth) tuple. wday is the index of the
|
||||
weekday (0-6, 0=Mon), and nth is the number of weeks to add
|
||||
forward or backward, depending on its signal. Notice that if
|
||||
the calculated date is already Monday, for example, using
|
||||
(0, 1) or (0, -1) won't change the day.
|
||||
"""
|
||||
|
||||
def __init__(self, dt1=None, dt2=None,
|
||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||
year=None, month=None, day=None, weekday=None,
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
if dt1 and dt2:
|
||||
if not isinstance(dt1, datetime.date) or \
|
||||
not isinstance(dt2, datetime.date):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
if type(dt1) is not type(dt2):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||
self.years = 0
|
||||
self.months = 0
|
||||
self.days = 0
|
||||
self.leapdays = 0
|
||||
self.hours = 0
|
||||
self.minutes = 0
|
||||
self.seconds = 0
|
||||
self.microseconds = 0
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.weekday = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
if dt1 < dt2:
|
||||
while dt1 > dtm:
|
||||
months += 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
else:
|
||||
while dt1 < dtm:
|
||||
months -= 1
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
delta = dt1 - dtm
|
||||
self.seconds = delta.seconds+delta.days*86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
self.years = years
|
||||
self.months = months
|
||||
self.days = days+weeks*7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self.seconds = seconds
|
||||
self.microseconds = microseconds
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if type(weekday) is int:
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
self.weekday = weekday
|
||||
|
||||
yday = 0
|
||||
if nlyearday:
|
||||
yday = nlyearday
|
||||
elif yearday:
|
||||
yday = yearday
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
if idx == 0:
|
||||
self.day = yday
|
||||
else:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
s = self.microseconds//abs(self.microseconds)
|
||||
div, mod = divmod(self.microseconds*s, 1000000)
|
||||
self.microseconds = mod*s
|
||||
self.seconds += div*s
|
||||
if abs(self.seconds) > 59:
|
||||
s = self.seconds//abs(self.seconds)
|
||||
div, mod = divmod(self.seconds*s, 60)
|
||||
self.seconds = mod*s
|
||||
self.minutes += div*s
|
||||
if abs(self.minutes) > 59:
|
||||
s = self.minutes//abs(self.minutes)
|
||||
div, mod = divmod(self.minutes*s, 60)
|
||||
self.minutes = mod*s
|
||||
self.hours += div*s
|
||||
if abs(self.hours) > 23:
|
||||
s = self.hours//abs(self.hours)
|
||||
div, mod = divmod(self.hours*s, 24)
|
||||
self.hours = mod*s
|
||||
self.days += div*s
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years += div*s
|
||||
if (self.hours or self.minutes or self.seconds or self.microseconds or
|
||||
self.hour is not None or self.minute is not None or
|
||||
self.second is not None or self.microsecond is not None):
|
||||
self._has_time = 1
|
||||
else:
|
||||
self._has_time = 0
|
||||
|
||||
def _set_months(self, months):
|
||||
self.months = months
|
||||
if abs(self.months) > 11:
|
||||
s = self.months//abs(self.months)
|
||||
div, mod = divmod(self.months*s, 12)
|
||||
self.months = mod*s
|
||||
self.years = div*s
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def __radd__(self, other):
|
||||
if not isinstance(other, datetime.date):
|
||||
raise TypeError("unsupported type for add operation")
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
month = self.month or other.month
|
||||
if self.months:
|
||||
assert 1 <= abs(self.months) <= 12
|
||||
month += self.months
|
||||
if month > 12:
|
||||
year += 1
|
||||
month -= 12
|
||||
elif month < 1:
|
||||
year -= 1
|
||||
month += 12
|
||||
day = min(calendar.monthrange(year, month)[1],
|
||||
self.day or other.day)
|
||||
repl = {"year": year, "month": month, "day": day}
|
||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
repl[attr] = value
|
||||
days = self.days
|
||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||
days += self.leapdays
|
||||
ret = (other.replace(**repl)
|
||||
+ datetime.timedelta(days=days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds,
|
||||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth)-1)*7
|
||||
if nth > 0:
|
||||
jumpdays += (7-ret.weekday()+weekday)%7
|
||||
else:
|
||||
jumpdays += (ret.weekday()-weekday)%7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __add__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError("unsupported type for add operation")
|
||||
return relativedelta(years=other.years+self.years,
|
||||
months=other.months+self.months,
|
||||
days=other.days+self.days,
|
||||
hours=other.hours+self.hours,
|
||||
minutes=other.minutes+self.minutes,
|
||||
seconds=other.seconds+self.seconds,
|
||||
microseconds=other.microseconds+self.microseconds,
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=other.second or self.microsecond)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError("unsupported type for sub operation")
|
||||
return relativedelta(years=other.years-self.years,
|
||||
months=other.months-self.months,
|
||||
days=other.days-self.days,
|
||||
hours=other.hours-self.hours,
|
||||
minutes=other.minutes-self.minutes,
|
||||
seconds=other.seconds-self.seconds,
|
||||
microseconds=other.microseconds-self.microseconds,
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=other.second or self.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return relativedelta(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
minutes=-self.minutes,
|
||||
seconds=-self.seconds,
|
||||
microseconds=-self.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __nonzero__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
not self.hours and
|
||||
not self.minutes and
|
||||
not self.seconds and
|
||||
not self.microseconds and
|
||||
not self.leapdays and
|
||||
self.year is None and
|
||||
self.month is None and
|
||||
self.day is None and
|
||||
self.weekday is None and
|
||||
self.hour is None and
|
||||
self.minute is None and
|
||||
self.second is None and
|
||||
self.microsecond is None)
|
||||
|
||||
def __mul__(self, other):
|
||||
f = float(other)
|
||||
return relativedelta(years=self.years*f,
|
||||
months=self.months*f,
|
||||
days=self.days*f,
|
||||
hours=self.hours*f,
|
||||
minutes=self.minutes*f,
|
||||
seconds=self.seconds*f,
|
||||
microseconds=self.microseconds*f,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return False
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
if self.weekday.weekday != other.weekday.weekday:
|
||||
return False
|
||||
n1, n2 = self.weekday.n, other.weekday.n
|
||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||
return False
|
||||
return (self.years == other.years and
|
||||
self.months == other.months and
|
||||
self.days == other.days and
|
||||
self.hours == other.hours and
|
||||
self.minutes == other.minutes and
|
||||
self.seconds == other.seconds and
|
||||
self.leapdays == other.leapdays and
|
||||
self.year == other.year and
|
||||
self.month == other.month and
|
||||
self.day == other.day and
|
||||
self.hour == other.hour and
|
||||
self.minute == other.minute and
|
||||
self.second == other.second and
|
||||
self.microsecond == other.microsecond)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __div__(self, other):
|
||||
return self.__mul__(1/float(other))
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if value:
|
||||
l.append("%s=%+d" % (attr, value))
|
||||
for attr in ["year", "month", "day", "weekday",
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, value))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
194
resources/lib/scheduler.py
Normal file
@@ -0,0 +1,194 @@
|
||||
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()
|
||||
@@ -1,11 +1,12 @@
|
||||
import urllib2
|
||||
# this is duplicated in snipppets of code from all over the web, credit to no one
|
||||
# in particular - to all those that have gone before me!
|
||||
from future.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
#this is duplicated in snipppets of code from all over the web, credit to no one
|
||||
#in particular - to all those that have gone before me!
|
||||
def shorten(aUrl):
|
||||
tinyurl = 'http://tinyurl.com/api-create.php?url='
|
||||
req = urllib2.urlopen(tinyurl + aUrl)
|
||||
req = urlopen(tinyurl + aUrl)
|
||||
data = req.read()
|
||||
|
||||
#should be a tiny url
|
||||
return str(data)
|
||||
# should be a tiny url
|
||||
return data
|
||||
|
||||
@@ -1,48 +1,69 @@
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
|
||||
__addon_id__= 'script.xbmcbackup'
|
||||
__addon_id__ = 'script.xbmcbackup'
|
||||
__Addon = xbmcaddon.Addon(__addon_id__)
|
||||
|
||||
|
||||
def data_dir():
|
||||
return __Addon.getAddonInfo('profile')
|
||||
|
||||
|
||||
def addon_dir():
|
||||
return __Addon.getAddonInfo('path')
|
||||
|
||||
|
||||
def openSettings():
|
||||
__Addon.openSettings()
|
||||
|
||||
def log(message,loglevel=xbmc.LOGDEBUG):
|
||||
xbmc.log(encode(__addon_id__ + "-" + __Addon.getAddonInfo('version') + ": " + message),level=loglevel)
|
||||
|
||||
def log(message, loglevel=xbmc.LOGDEBUG):
|
||||
xbmc.log(__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/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 setSetting(name,value):
|
||||
__Addon.setSetting(name,value)
|
||||
|
||||
|
||||
def getSettingBool(name):
|
||||
return bool(__Addon.getSettingBool(name))
|
||||
|
||||
|
||||
def getSettingInt(name):
|
||||
return __Addon.getSettingInt(name)
|
||||
|
||||
|
||||
def setSetting(name, value):
|
||||
__Addon.setSetting(name, value)
|
||||
|
||||
|
||||
def getString(string_id):
|
||||
return __Addon.getLocalizedString(string_id)
|
||||
|
||||
def getRegionalTimestamp(date_time,dateformat=['dateshort']):
|
||||
|
||||
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 = ''
|
||||
|
||||
try:
|
||||
result = string.encode('UTF-8','replace')
|
||||
except UnicodeDecodeError:
|
||||
result = 'Unicode Error'
|
||||
|
||||
return result
|
||||
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])
|
||||
|
||||
@@ -1,126 +1,142 @@
|
||||
import utils as utils
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
from __future__ import unicode_literals
|
||||
import zipfile
|
||||
import os.path
|
||||
import sys
|
||||
import dropbox
|
||||
from dropbox.files import WriteMode,CommitInfo,UploadSessionCursor
|
||||
from authorizers import DropboxAuthorizer,GoogleDriveAuthorizer
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
from dropbox import dropbox
|
||||
from . import utils as utils
|
||||
from dropbox.files import WriteMode, CommitInfo, UploadSessionCursor
|
||||
from . authorizers import DropboxAuthorizer
|
||||
|
||||
|
||||
class Vfs:
|
||||
root_path = None
|
||||
|
||||
def __init__(self,rootString):
|
||||
def __init__(self, rootString):
|
||||
self.set_root(rootString)
|
||||
|
||||
def set_root(self,rootString):
|
||||
old_root = self.root_path
|
||||
self.root_path = rootString
|
||||
|
||||
#fix slashes
|
||||
self.root_path = self.root_path.replace("\\","/")
|
||||
|
||||
#check if trailing slash is included
|
||||
if(self.root_path[-1:] != "/"):
|
||||
self.root_path = self.root_path + "/"
|
||||
|
||||
#return the old root
|
||||
def clean_path(self, path):
|
||||
# fix slashes
|
||||
path = path.replace("\\", "/")
|
||||
|
||||
# check if trailing slash is included
|
||||
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
|
||||
|
||||
def listdir(self,directory):
|
||||
|
||||
def listdir(self, directory):
|
||||
return {}
|
||||
|
||||
def mkdir(self,directory):
|
||||
def mkdir(self, directory):
|
||||
return True
|
||||
|
||||
def put(self,source,dest):
|
||||
def put(self, source, dest):
|
||||
return True
|
||||
|
||||
def rmdir(self,directory):
|
||||
def rmdir(self, directory):
|
||||
return True
|
||||
|
||||
def rmfile(self,aFile):
|
||||
def rmfile(self, aFile):
|
||||
return True
|
||||
|
||||
def exists(self,aFile):
|
||||
def exists(self, aFile):
|
||||
return True
|
||||
|
||||
def rename(self,aFile,newName):
|
||||
|
||||
def rename(self, aFile, newName):
|
||||
return True
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
return True
|
||||
|
||||
|
||||
def fileSize(self, filename):
|
||||
return 0 # result should be in KB
|
||||
|
||||
|
||||
class XBMCFileSystem(Vfs):
|
||||
|
||||
def listdir(self,directory):
|
||||
def listdir(self, directory):
|
||||
return xbmcvfs.listdir(directory)
|
||||
|
||||
def mkdir(self,directory):
|
||||
return xbmcvfs.mkdir(xbmc.translatePath(directory))
|
||||
def mkdir(self, directory):
|
||||
return xbmcvfs.mkdir(xbmcvfs.translatePath(directory))
|
||||
|
||||
def put(self,source,dest):
|
||||
return xbmcvfs.copy(xbmc.translatePath(source),xbmc.translatePath(dest))
|
||||
|
||||
def rmdir(self,directory):
|
||||
return xbmcvfs.rmdir(directory,True)
|
||||
def put(self, source, dest):
|
||||
return xbmcvfs.copy(xbmcvfs.translatePath(source), xbmcvfs.translatePath(dest))
|
||||
|
||||
def rmfile(self,aFile):
|
||||
def rmdir(self, directory):
|
||||
return xbmcvfs.rmdir(directory)
|
||||
|
||||
def rmfile(self, aFile):
|
||||
return xbmcvfs.delete(aFile)
|
||||
|
||||
def rename(self,aFile,newName):
|
||||
def rename(self, aFile, newName):
|
||||
return xbmcvfs.rename(aFile, newName)
|
||||
|
||||
def exists(self,aFile):
|
||||
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
|
||||
|
||||
def __init__(self,rootString,mode):
|
||||
|
||||
def __init__(self, rootString, mode):
|
||||
self.root_path = ""
|
||||
self.zip = zipfile.ZipFile(rootString,mode=mode,compression=zipfile.ZIP_DEFLATED,allowZip64=True)
|
||||
|
||||
def listdir(self,directory):
|
||||
return [[],[]]
|
||||
|
||||
def mkdir(self,directory):
|
||||
#self.zip.write(directory[len(self.root_path):])
|
||||
self.zip = zipfile.ZipFile(rootString, mode=mode, compression=zipfile.ZIP_DEFLATED, allowZip64=True)
|
||||
|
||||
def listdir(self, directory):
|
||||
return [[], []]
|
||||
|
||||
def mkdir(self, directory):
|
||||
# self.zip.write(directory[len(self.root_path):])
|
||||
return False
|
||||
|
||||
def put(self,source,dest):
|
||||
|
||||
aFile = xbmcvfs.File(xbmc.translatePath(source),'r')
|
||||
|
||||
self.zip.writestr(utils.encode(dest),aFile.read())
|
||||
|
||||
|
||||
def put(self, source, dest):
|
||||
|
||||
aFile = xbmcvfs.File(xbmcvfs.translatePath(source), 'r')
|
||||
|
||||
self.zip.writestr(dest, aFile.readBytes())
|
||||
|
||||
return True
|
||||
|
||||
def rmdir(self,directory):
|
||||
|
||||
def rmdir(self, directory):
|
||||
return False
|
||||
|
||||
def exists(self,aFile):
|
||||
|
||||
def exists(self, aFile):
|
||||
return False
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
self.zip.close()
|
||||
|
||||
def extract(self,aFile,path):
|
||||
#extract zip file to path
|
||||
self.zip.extract(aFile,path)
|
||||
|
||||
|
||||
def extract(self, aFile, path):
|
||||
# extract zip file to path
|
||||
self.zip.extract(aFile, path)
|
||||
|
||||
def listFiles(self):
|
||||
return self.zip.infolist()
|
||||
|
||||
|
||||
class DropboxFileSystem(Vfs):
|
||||
MAX_CHUNK = 50 * 1000 * 1000 #dropbox uses 150, reduced to 50 for small mem systems
|
||||
MAX_CHUNK = 50 * 1000 * 1000 # dropbox uses 150, reduced to 50 for small mem systems
|
||||
client = None
|
||||
APP_KEY = ''
|
||||
APP_SECRET = ''
|
||||
|
||||
def __init__(self,rootString):
|
||||
|
||||
def __init__(self, rootString):
|
||||
self.set_root(rootString)
|
||||
|
||||
authorizer = DropboxAuthorizer()
|
||||
@@ -128,325 +144,146 @@ class DropboxFileSystem(Vfs):
|
||||
if(authorizer.isAuthorized()):
|
||||
self.client = authorizer.getClient()
|
||||
else:
|
||||
#tell the user to go back and run the authorizer
|
||||
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30105))
|
||||
# tell the user to go back and run the authorizer
|
||||
xbmcgui.Dialog().ok(utils.getString(30010), utils.getString(30105))
|
||||
sys.exit()
|
||||
|
||||
def listdir(self,directory):
|
||||
def listdir(self, directory):
|
||||
directory = self._fix_slashes(directory)
|
||||
|
||||
if(self.client != None and self.exists(directory)):
|
||||
|
||||
if(self.client is not None and self.exists(directory)):
|
||||
files = []
|
||||
dirs = []
|
||||
metadata = self.client.files_list_folder(directory)
|
||||
|
||||
for aFile in metadata.entries:
|
||||
if(isinstance(aFile,dropbox.files.FolderMetadata)):
|
||||
dirs.append(utils.encode(aFile.name))
|
||||
if(isinstance(aFile, dropbox.files.FolderMetadata)):
|
||||
dirs.append(aFile.name)
|
||||
else:
|
||||
files.append(utils.encode(aFile.name))
|
||||
files.append(aFile.name)
|
||||
|
||||
return [dirs,files]
|
||||
return [dirs, files]
|
||||
else:
|
||||
return [[],[]]
|
||||
|
||||
return [[], []]
|
||||
|
||||
def mkdir(self,directory):
|
||||
def mkdir(self, directory):
|
||||
directory = self._fix_slashes(directory)
|
||||
if(self.client != None):
|
||||
#sort of odd but always return true, folder create is implicit with file upload
|
||||
if(self.client is not None):
|
||||
# sort of odd but always return true, folder create is implicit with file upload
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def rmdir(self,directory):
|
||||
def rmdir(self, directory):
|
||||
directory = self._fix_slashes(directory)
|
||||
if(self.client != None and self.exists(directory)):
|
||||
#dropbox is stupid and will refuse to do this sometimes, need to delete recursively
|
||||
dirs,files = self.listdir(directory)
|
||||
|
||||
if(self.client is not None and self.exists(directory)):
|
||||
# dropbox is stupid and will refuse to do this sometimes, need to delete recursively
|
||||
dirs, files = self.listdir(directory)
|
||||
|
||||
for aDir in dirs:
|
||||
self.rmdir(aDir)
|
||||
|
||||
#finally remove the root directory
|
||||
# finally remove the root directory
|
||||
self.client.files_delete(directory)
|
||||
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def rmfile(self,aFile):
|
||||
def rmfile(self, aFile):
|
||||
aFile = self._fix_slashes(aFile)
|
||||
|
||||
if(self.client != None and self.exists(aFile)):
|
||||
|
||||
if(self.client is not None and self.exists(aFile)):
|
||||
self.client.files_delete(aFile)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def exists(self,aFile):
|
||||
def exists(self, aFile):
|
||||
aFile = self._fix_slashes(aFile)
|
||||
|
||||
if(self.client != None):
|
||||
#can't list root metadata
|
||||
|
||||
if(self.client is not None):
|
||||
# can't list root metadata
|
||||
if(aFile == ''):
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
meta_data = self.client.files_get_metadata(aFile)
|
||||
#if we make it here the file does exist
|
||||
self.client.files_get_metadata(aFile)
|
||||
# if we make it here the file does exist
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def put(self,source,dest,retry=True):
|
||||
def put(self, source, dest, retry=True):
|
||||
dest = self._fix_slashes(dest)
|
||||
|
||||
if(self.client != None):
|
||||
#open the file and get its size
|
||||
f = open(source,'rb')
|
||||
|
||||
if(self.client is not None):
|
||||
# open the file and get its size
|
||||
f = open(source, 'rb')
|
||||
f_size = os.path.getsize(source)
|
||||
|
||||
|
||||
try:
|
||||
if(f_size < self.MAX_CHUNK):
|
||||
#use the regular upload
|
||||
response = self.client.files_upload(f.read(),dest,mode=WriteMode('overwrite'))
|
||||
# use the regular upload
|
||||
self.client.files_upload(f.read(), dest, mode=WriteMode('overwrite'))
|
||||
else:
|
||||
#start the upload session
|
||||
# start the upload session
|
||||
upload_session = self.client.files_upload_session_start(f.read(self.MAX_CHUNK))
|
||||
upload_cursor = UploadSessionCursor(upload_session.session_id,f.tell())
|
||||
|
||||
upload_cursor = UploadSessionCursor(upload_session.session_id, f.tell())
|
||||
|
||||
while(f.tell() < f_size):
|
||||
#check if we should finish the upload
|
||||
# check if we should finish the upload
|
||||
if((f_size - f.tell()) <= self.MAX_CHUNK):
|
||||
#upload and close
|
||||
self.client.files_upload_session_finish(f.read(self.MAX_CHUNK),upload_cursor,CommitInfo(dest,mode=WriteMode('overwrite')))
|
||||
# upload and close
|
||||
self.client.files_upload_session_finish(f.read(self.MAX_CHUNK), upload_cursor, CommitInfo(dest, mode=WriteMode('overwrite')))
|
||||
else:
|
||||
#upload a part and store the offset
|
||||
self.client.files_upload_session_append_v2(f.read(self.MAX_CHUNK),upload_cursor)
|
||||
# upload a part and store the offset
|
||||
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))
|
||||
|
||||
#if we have an exception retry
|
||||
|
||||
# if we have an exception retry
|
||||
if(retry):
|
||||
return self.put(source,dest,False)
|
||||
return self.put(source, dest, False)
|
||||
else:
|
||||
#tried once already, just quit
|
||||
# tried once already, just quit
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_file(self,source,dest):
|
||||
if(self.client != None):
|
||||
#write the file locally
|
||||
f = self.client.files_download_to_file(dest,source)
|
||||
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
|
||||
self.client.files_download_to_file(dest, source)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _fix_slashes(self,filename):
|
||||
result = filename.replace('\\','/')
|
||||
def _fix_slashes(self, filename):
|
||||
result = filename.replace('\\', '/')
|
||||
|
||||
#root needs to be a blank string
|
||||
# root needs to be a blank string
|
||||
if(result == '/'):
|
||||
result = ""
|
||||
|
||||
#if dir ends in slash, remove it
|
||||
# if dir ends in slash, remove it
|
||||
if(result[-1:] == "/"):
|
||||
result = result[:-1]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class GoogleDriveFilesystem(Vfs):
|
||||
drive = None
|
||||
history = {}
|
||||
FOLDER_TYPE = 'application/vnd.google-apps.folder'
|
||||
|
||||
def __init__(self,rootString):
|
||||
self.set_root(rootString)
|
||||
|
||||
authorizer = GoogleDriveAuthorizer()
|
||||
|
||||
if(authorizer.isAuthorized()):
|
||||
self.drive = authorizer.getClient()
|
||||
else:
|
||||
#tell the user to go back and run the authorizer
|
||||
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30105))
|
||||
sys.exit()
|
||||
|
||||
#make sure we have the folder we need
|
||||
xbmc_folder = self._getGoogleFile(self.root_path)
|
||||
if(xbmc_folder == None):
|
||||
self.mkdir(self.root_path)
|
||||
|
||||
def listdir(self,directory):
|
||||
files = []
|
||||
dirs = []
|
||||
|
||||
if(not directory.startswith('/')):
|
||||
directory = '/' + directory
|
||||
|
||||
#get the id of this folder
|
||||
parentFolder = self._getGoogleFile(directory)
|
||||
|
||||
#need to do this after
|
||||
if(not directory.endswith('/')):
|
||||
directory = directory + '/'
|
||||
|
||||
if(parentFolder != None):
|
||||
|
||||
fileList = self.drive.ListFile({'q':"'" + parentFolder['id'] + "' in parents and trashed = false"}).GetList()
|
||||
|
||||
for aFile in fileList:
|
||||
if(aFile['mimeType'] == self.FOLDER_TYPE):
|
||||
dirs.append(utils.encode(aFile['title']))
|
||||
else:
|
||||
files.append(utils.encode(aFile['title']))
|
||||
|
||||
|
||||
return [dirs,files]
|
||||
|
||||
def mkdir(self,directory):
|
||||
result = True
|
||||
|
||||
if(not directory.startswith('/')):
|
||||
directory = '/' + directory
|
||||
|
||||
if(directory.endswith('/')):
|
||||
directory = directory[:-1]
|
||||
|
||||
#split the string by the directory separator
|
||||
pathList = os.path.split(directory)
|
||||
|
||||
if(pathList[0] == '/'):
|
||||
|
||||
#we're at the root, just make the folder
|
||||
newFolder = self.drive.CreateFile({'title': pathList[1], 'parent':'root','mimeType':self.FOLDER_TYPE})
|
||||
newFolder.Upload()
|
||||
else:
|
||||
#get the id of the parent folder
|
||||
parentFolder = self._getGoogleFile(pathList[0])
|
||||
|
||||
if(parentFolder != None):
|
||||
newFolder = self.drive.CreateFile({'title': pathList[1],"parents":[{'kind':'drive#fileLink','id':parentFolder['id']}],'mimeType':self.FOLDER_TYPE})
|
||||
newFolder.Upload()
|
||||
else:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def put(self,source,dest):
|
||||
result = True
|
||||
|
||||
#make the name separate from the path
|
||||
if(not dest.startswith('/')):
|
||||
dest = '/' + dest
|
||||
|
||||
pathList = os.path.split(dest)
|
||||
|
||||
#get the parent location
|
||||
parentFolder = self._getGoogleFile(pathList[0])
|
||||
|
||||
if(parentFolder != None):
|
||||
#create a new file in this folder
|
||||
newFile = self.drive.CreateFile({"title":pathList[1],"parents":[{'kind':'drive#fileLink','id':parentFolder['id']}]})
|
||||
newFile.SetContentFile(source)
|
||||
newFile.Upload()
|
||||
else:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def get_file(self,source, dest):
|
||||
result = True
|
||||
|
||||
#get the id of this file
|
||||
file = self._getGoogleFile(source)
|
||||
|
||||
if(file != None):
|
||||
file.GetContentFile(dest)
|
||||
else:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def rmdir(self,directory):
|
||||
result = True
|
||||
|
||||
#check that the folder exists
|
||||
folder = self._getGoogleFile(directory)
|
||||
|
||||
if(folder != None):
|
||||
#delete the folder
|
||||
folder.Delete()
|
||||
else:
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def rmfile(self,aFile):
|
||||
#really just the same as the remove directory function
|
||||
return self.rmdir(aFile)
|
||||
|
||||
def exists(self,aFile):
|
||||
#attempt to get this file
|
||||
foundFile = self._getGoogleFile(aFile)
|
||||
|
||||
if(foundFile != None):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def rename(self,aFile,newName):
|
||||
return True
|
||||
|
||||
def _getGoogleFile(self,file):
|
||||
result = None
|
||||
|
||||
#file must start with / and not end with one (even directory)
|
||||
if(not file.startswith('/')):
|
||||
file = '/' + file
|
||||
|
||||
if(file.endswith('/')):
|
||||
file = file[:-1]
|
||||
|
||||
if(self.history.has_key(file)):
|
||||
|
||||
result = self.history[file]
|
||||
else:
|
||||
pathList = os.path.split(file)
|
||||
|
||||
#end of recurision, we got the root
|
||||
if(pathList[0] == '/'):
|
||||
#get the id of this file (if it exists)
|
||||
file_list = self.drive.ListFile({'q':"title='" + pathList[1] + "' and 'root' in parents and trashed=false"}).GetList()
|
||||
|
||||
if(len(file_list) > 0):
|
||||
result = file_list[0]
|
||||
self.history[pathList[1]] = result
|
||||
else:
|
||||
#recurse down the tree
|
||||
current_file = pathList[1]
|
||||
|
||||
parentId = self._getGoogleFile(pathList[0])
|
||||
|
||||
if(parentId != None):
|
||||
self.history[pathList[0]] = parentId
|
||||
|
||||
#attempt to get the id of this file, with this parent
|
||||
file_list = file_list = self.drive.ListFile({'q':"title='" + current_file + "' and '" + parentId['id'] + "' in parents and trashed=false"}).GetList()
|
||||
|
||||
if(len(file_list) > 0):
|
||||
result = file_list[0]
|
||||
self.history[file] = result
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,43 +1,392 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<settings>
|
||||
<category id="general" label="30011">
|
||||
<setting id="compress_backups" type="bool" label="30087" default="false" />
|
||||
<setting id="backup_rotation" type="number" label="30026" default="0" />
|
||||
<setting id="progress_mode" type="enum" label="30022" lvalues="30082|30083|30084" default="0" />
|
||||
<setting id="upgrade_notes" type="number" label="upgrade_notes" visible="false" default="1" />
|
||||
</category>
|
||||
<category id="backup_path" label="30048">
|
||||
<setting id="remote_selection" type="enum" lvalues="30018|30019|30027|30098" default="0" label="30025"/>
|
||||
<setting id="remote_path_2" type="text" label="30024" default="" visible="eq(-1,1)" />
|
||||
<setting id="remote_path" type="folder" label="30020" visible="eq(-2,0)" />
|
||||
<setting id="dropbox_key" type="text" label="30028" visible="eq(-3,2)" default="" />
|
||||
<setting id="dropbox_secret" type="text" label="30029" visible="eq(-4,2)" default="" />
|
||||
<setting id="google_drive_id" type="text" label="Client ID" visible="eq(-5,3)" default="" />
|
||||
<setting id="google_drive_secret" type="text" label="Client Secret" visible="eq(-6,3)" default="" />
|
||||
<setting id="auth_dropbox_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=authorize_cloud&provider=dropbox)" visible="eq(-7,2)"/>
|
||||
<setting id="auth_google_button" type="action" label="30104" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=authorize_cloud&provider=google_drive)" visible="eq(-8,3)"/>
|
||||
<setting id="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=remove_auth)" visible="gt(-9,1)"/>
|
||||
</category>
|
||||
<category id="selection" label="30012">
|
||||
<setting id="backup_selection_type" type="enum" lvalues="30014|30015" default="0" label="30023" />
|
||||
<setting id="backup_addon_data" type="bool" label="30031" default="false" visible="eq(-1,0)"/>
|
||||
<setting id="backup_config" type="bool" label="30035" default="true" visible="eq(-2,0)"/>
|
||||
<setting id="backup_database" type="bool" label="30032" default="true" visible="eq(-3,0)"/>
|
||||
<setting id="backup_game_saves" type="bool" label="30133" default="false" visible="eq(-4,0)" />
|
||||
<setting id="backup_playlists" type="bool" label="30033" default="true" visible="eq(-5,0)"/>
|
||||
<setting id="backup_profiles" type="bool" label="30080" default="false" visible="eq(-6,0)"/>
|
||||
<setting id="backup_thumbnails" type="bool" label="30034" default="true" visible="eq(-7,0)"/>
|
||||
<setting id="backup_addons" type="bool" label="30030" default="true" visible="eq(-8,0)" />
|
||||
<setting id="advanced_button" type="action" label="30125" visible="eq(-9,1)" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_editor)" />
|
||||
<setting id="advanced_defaults" type="action" label="30139" visible="eq(-10,1)" action="RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_copy_config)" />
|
||||
</category>
|
||||
<category id="scheduling" label="30013">
|
||||
<setting id="enable_scheduler" type="bool" label="30060" default="false" />
|
||||
<setting id="schedule_interval" type="enum" label="30061" lvalues="30079|30072|30073|30074|30075" default="1" enable="eq(-1,true)"/>
|
||||
<setting id="schedule_time" type="labelenum" label="30062" values="00:00|01:00|02:00|03:00|04:00|05:00|06:00|07:00|08:00|09:00|10:00|11:00|12:00|13:00|14:00|15:00|16:00|17:00|18:00|19:00|20:00|21:00|22:00|23:00" default="00:00" visible="!eq(-1,4)" enable="eq(-2,true)"/>
|
||||
<setting id="day_of_week" type="enum" label="30063" lvalues="30065|30066|30067|30068|30069|30070|30071" default="0" visible="eq(-2,2)" enable="eq(-3,true)"/>
|
||||
<setting id="cron_schedule" type="text" label="30064" default="0 0 * * *" visible="eq(-3,4)" enable="eq(-4,true)"/>
|
||||
<setting id="schedule_miss" type="bool" label="30109" default="false" enable="eq(-5,true)" />
|
||||
<setting id="cron_shutdown" type="bool" label="30076" default="false" enable="eq(-6,true)" />
|
||||
</category>
|
||||
<?xml version="1.0"?>
|
||||
<settings version="1">
|
||||
<section id="service.xbmcbackup">
|
||||
<category id="general" label="30011" help="">
|
||||
<group id="1" label="">
|
||||
<!-- compress backups -->
|
||||
<setting id="compress_backups" type="boolean" label="30087" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- zip folder staging path -->
|
||||
<setting id="zip_temp_path" type="string" label="30152" help="30153">
|
||||
<level>3</level>
|
||||
<default>special://temp</default>
|
||||
<constraints>
|
||||
<allowempty>true</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="compress_backups">true</dependency>
|
||||
</dependencies>
|
||||
<control type="edit" format="string">
|
||||
<heading>30152</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- backup rotation -->
|
||||
<setting id="backup_rotation" type="integer" label="30026" help="">
|
||||
<level>0</level>
|
||||
<default>0</default>
|
||||
<control type="edit" format="integer">
|
||||
<heading>30026</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- prompt to restore settings -->
|
||||
<setting id="always_prompt_restore_settings" type="boolean" label="30148" help="30154">
|
||||
<level>2</level>
|
||||
<default>false</default>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- progress mode -->
|
||||
<setting id="progress_mode" type="integer" label="30022" help="">
|
||||
<level>1</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30082">0</option>
|
||||
<option label="30083">1</option>
|
||||
<option label="30084">2</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control type="spinner" format="string" />
|
||||
</setting>
|
||||
</group>
|
||||
<group id="2" label="">
|
||||
<!-- verbose logging -->
|
||||
<setting id="verbose_logging" type="boolean" label="30151" help="30155">
|
||||
<level>3</level>
|
||||
<default>false</default>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- upgrade notes not visible to users -->
|
||||
<setting id="upgrade_notes" type="integer" label="upgrade_notes" help="">
|
||||
<level>4</level>
|
||||
<default>1</default>
|
||||
<visible>false</visible>
|
||||
<control type="edit" format="integer">
|
||||
<heading>upgrade_notes</heading>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
<category id="backup_path" label="30048" help="">
|
||||
<group id="1" label="">
|
||||
<!-- backup repo type -->
|
||||
<setting id="remote_selection" type="integer" label="30025" help="">
|
||||
<level>0</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30018">0</option>
|
||||
<option label="30019">1</option>
|
||||
<option label="30027">2</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control type="spinner" format="string" />
|
||||
</setting>
|
||||
<!-- folder select path -->
|
||||
<setting id="remote_path" type="path" label="30020" help="">
|
||||
<level>0</level>
|
||||
<default/>
|
||||
<constraints>
|
||||
<allowempty>true</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">0</dependency>
|
||||
</dependencies>
|
||||
<control type="button" format="path">
|
||||
<heading>30020</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- type remote path -->
|
||||
<setting id="remote_path_2" type="string" label="30024" help="">
|
||||
<level>0</level>
|
||||
<default></default>
|
||||
<constraints>
|
||||
<allowempty>true</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">1</dependency>
|
||||
</dependencies>
|
||||
<control type="edit" format="string">
|
||||
<heading>30024</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- dropbox key and secret -->
|
||||
<setting id="dropbox_key" type="string" label="30028" help="30156">
|
||||
<level>0</level>
|
||||
<default></default>
|
||||
<constraints>
|
||||
<allowempty>true</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">2</dependency>
|
||||
</dependencies>
|
||||
<control type="edit" format="string">
|
||||
<heading>30028</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<setting id="dropbox_secret" type="string" label="30029" help="30156">
|
||||
<level>0</level>
|
||||
<default></default>
|
||||
<constraints>
|
||||
<allowempty>true</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">2</dependency>
|
||||
</dependencies>
|
||||
<control type="edit" format="string">
|
||||
<heading>30029</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- authorize dropbox -->
|
||||
<setting id="auth_dropbox_button" type="action" label="30104" help="">
|
||||
<level>0</level>
|
||||
<default />
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">2</dependency>
|
||||
</dependencies>
|
||||
<control type="button" format="action">
|
||||
<data>RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=authorize_cloud,provider=dropbox)</data>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
<group id="2" label="">
|
||||
<!-- remove auth button -->
|
||||
<setting id="remove_auth_button" type="action" label="30093" help="">
|
||||
<level>2</level>
|
||||
<default />
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="remote_selection">2</dependency>
|
||||
</dependencies>
|
||||
<control type="button" format="action">
|
||||
<data>RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=remove_auth)</data>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
<category id="selection" label="30012">
|
||||
<group id="1" label="">
|
||||
<!-- selection type (simple/advanced) -->
|
||||
<setting id="backup_selection_type" type="integer" label="30023" help="30157">
|
||||
<level>2</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30014">0</option>
|
||||
<option label="30015">1</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control type="spinner" format="string" />
|
||||
</setting>
|
||||
<!-- simple selection settings -->
|
||||
<setting id="backup_addon_data" type="boolean" label="30031" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_config" type="boolean" label="30035" help="">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_database" type="boolean" label="30032" help="">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_game_saves" type="boolean" label="30133" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_playlists" type="boolean" label="30033" help="">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_profiles" type="boolean" label="30080" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_thumbnails" type="boolean" label="30034" help="">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<setting id="backup_addons" type="boolean" label="30030" help="">
|
||||
<level>0</level>
|
||||
<default>true</default>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">0</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- advanced editor options -->
|
||||
<setting id="advanced_button" type="action" label="30125" help="">
|
||||
<level>2</level>
|
||||
<default />
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">1</dependency>
|
||||
</dependencies>
|
||||
<control type="button" format="action">
|
||||
<data>RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_editor)</data>
|
||||
</control>
|
||||
</setting>
|
||||
<setting id="advanced_defaults" type="action" label="30139" help="">
|
||||
<level>2</level>
|
||||
<default />
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="backup_selection_type">1</dependency>
|
||||
</dependencies>
|
||||
<control type="button" format="action">
|
||||
<data>RunScript(special://home/addons/script.xbmcbackup/launcher.py,action=advanced_copy_config)</data>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
<category id="scheduling" label="30013">
|
||||
<group id="1" label="">
|
||||
<!-- enable scheduler -->
|
||||
<setting id="enable_scheduler" type="boolean" label="30060" help="30158">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- schedule interval -->
|
||||
<setting id="schedule_interval" type="integer" label="30061" help="">
|
||||
<level>0</level>
|
||||
<default>1</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30079">0</option>
|
||||
<option label="30072">1</option>
|
||||
<option label="30073">2</option>
|
||||
<option label="30074">3</option>
|
||||
<option label="30075">4</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="spinner" format="string"/>
|
||||
</setting>
|
||||
<!-- hour of the day -->
|
||||
<setting id="schedule_time" type="string" label="30062" help="">
|
||||
<level>0</level>
|
||||
<default>00:00</default>
|
||||
<constraints>
|
||||
<options sort="ascending">
|
||||
<option>00:00</option>
|
||||
<option>01:00</option>
|
||||
<option>02:00</option>
|
||||
<option>03:00</option>
|
||||
<option>04:00</option>
|
||||
<option>05:00</option>
|
||||
<option>06:00</option>
|
||||
<option>07:00</option>
|
||||
<option>08:00</option>
|
||||
<option>09:00</option>
|
||||
<option>10:00</option>
|
||||
<option>11:00</option>
|
||||
<option>12:00</option>
|
||||
<option>13:00</option>
|
||||
<option>14:00</option>
|
||||
<option>15:00</option>
|
||||
<option>16:00</option>
|
||||
<option>17:00</option>
|
||||
<option>18:00</option>
|
||||
<option>19:00</option>
|
||||
<option>20:00</option>
|
||||
<option>21:00</option>
|
||||
<option>22:00</option>
|
||||
<option>23:00</option>
|
||||
</options>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="schedule_interval" operator="!is">4</dependency>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="spinner" format="string"/>
|
||||
</setting>
|
||||
<!-- day of the week -->
|
||||
<setting id="day_of_week" type="integer" label="30063" help="">
|
||||
<level>0</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30065">0</option>
|
||||
<option label="30066">1</option>
|
||||
<option label="30067">2</option>
|
||||
<option label="30068">3</option>
|
||||
<option label="30069">4</option>
|
||||
<option label="30070">5</option>
|
||||
<option label="30071">6</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="schedule_interval">2</dependency>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="spinner" format="string"/>
|
||||
</setting>
|
||||
<!-- cron schedule -->
|
||||
<setting id="cron_schedule" type="string" label="30064" help="">
|
||||
<level>0</level>
|
||||
<default>0 0 * * *</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="schedule_interval">4</dependency>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="edit" format="string">
|
||||
<heading>30064</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<!-- run if schedule missed -->
|
||||
<setting id="schedule_miss" type="boolean" label="30109" help="">
|
||||
<level>1</level>
|
||||
<default>false</default>
|
||||
<dependencies>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
<!-- shutdown on complete -->
|
||||
<setting id="cron_shutdown" type="boolean" label="30076" help="">
|
||||
<level>1</level>
|
||||
<default>false</default>
|
||||
<dependencies>
|
||||
<dependency type="enable" setting="enable_scheduler">true</dependency>
|
||||
</dependencies>
|
||||
<control type="toggle" />
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
</section>
|
||||
</settings>
|
||||
|
||||
198
scheduler.py
@@ -1,198 +0,0 @@
|
||||
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 = 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()
|
||||
4
service.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from resources.lib.scheduler import BackupScheduler
|
||||
|
||||
# start the backup scheduler
|
||||
BackupScheduler().start()
|
||||