Merge pull request #67 from robweber/helix_google_drive

Helix google drive
This commit is contained in:
robweber 2015-04-27 15:31:30 -05:00
commit 7d2c8c54c6
16 changed files with 1594 additions and 16 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.pyo
*.pyc
.project
.pydevproject

View File

@ -47,6 +47,11 @@ Using Dropbox as a storage target adds a few steps the first time you wish to ru
Once you have your app key and secret add them to the settings. Kodi Backup now needs to have permission to access your Dropbox account. When you see the prompt regarding the Dropbox URL Authorization DO NOT click OK. Check your Kodi log file for a line from "script.xbmcbackup" containing the authorization URL. Cut/paste this into a browser and click Allow. Once this is done you can click "OK" in Kodi and proceed as normal. Kodi Backup will cache the authorization code so you only have to do this once, or if you revoke the Dropbox permissions.
Using Google Drive:
Using the Google Drive target is very similar to the Dropbox one. You must create a Google API project and authenticate your account via the id and secret. Instructions for enable the Google API for Google Drive can be found here (https://developers.google.com/drive/web/quickstart/quickstart-python). You'll need the client id and client secret generated for the addon settings. You only need to follow Step 1.
Once you have the client ID and Secret add them to the addon settings and run a backup. You'll get a notification that you need to enter your authorization code. Check your Kodi log file for a line from "script.xbmcbackup" containing the authorization URL. Cut/paste this into a browser and click Allow. Once this is done put the code from your browser into the pop-up dialog. The addon will cache these credentials so it should be a one-time authentication. From there the backup should start to run.
Scripting A Backup:

View File

@ -3,6 +3,11 @@
name="Backup" version="1.0.5" provider-name="robweber">
<requires>
<import addon="xbmc.python" version="2.19.0"/>
<import addon="script.module.httplib2" version="0.8.0" />
<import addon="script.module.oauth2client" version="1.4.7" />
<import addon="script.module.uritemplate" version="0.6" />
<import addon="script.module.yaml" version="3.11"/>
<import addon="script.module.googleapi" version="1.4.0" />
</requires>
<extension point="xbmc.python.script" library="default.py">
<provides>executable</provides>

View File

@ -9,3 +9,5 @@ shouldDelete = xbmcgui.Dialog().yesno(utils.getString(30093),utils.getString(300
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

View File

@ -89,4 +89,5 @@
<string id="30095">Do you want to do this?</string>
<string id="30096">Old Zip Archive could not be deleted</string>
<string id="30097">This needs to happen before a backup can run</string>
<string id="30098">Google Drive</string>
</strings>

View File

@ -4,7 +4,7 @@ import xbmcvfs
import utils as utils
import time
import json
from vfs import XBMCFileSystem,DropboxFileSystem,ZipFileSystem
from vfs import XBMCFileSystem,DropboxFileSystem,ZipFileSystem,GoogleDriveFilesystem
from resources.lib.guisettings import GuiSettingsManager
def folderSort(aKey):
@ -55,6 +55,9 @@ class XbmcBackup:
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
@ -69,7 +72,7 @@ class XbmcBackup:
#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")):
@ -456,8 +459,8 @@ class XbmcBackup:
else:
self._updateProgress()
wroteFile = True
if(isinstance(source,DropboxFileSystem)):
#if copying from dropbox we need the file handle, use get_file
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

View File

@ -0,0 +1,185 @@
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.

View File

View File

@ -0,0 +1,173 @@
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):
"""Delete value of this attribute."""
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']

View File

@ -0,0 +1,432 @@
import socket
import webbrowser
import httplib2
import oauth2client.clientsecrets as clientsecrets
from apiclient.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.file import CredentialsFileSymbolicLinkError
from oauth2client.tools import ClientRedirectHandler
from oauth2client.tools import ClientRedirectServer
from oauth2client.util 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, e:
pass
else:
success = True
break
if success:
oauth_callback = 'http://%s:%s/' % (host_name, port_number)
else:
print 'Failed to start a local webserver. Please check your firewall'
print 'settings and locally running programs that may be blocking or'
print 'using configured ports. Default ports are 8080 and 8090.'
raise AuthenticationError()
self.flow.redirect_uri = oauth_callback
authorize_url = self.GetAuthUrl()
webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:'
print
print ' ' + authorize_url
print
httpd.handle_request()
if 'error' in httpd.query_params:
print 'Authentication request was rejected'
raise AuthenticationRejected('User rejected authentication')
if 'code' in httpd.query_params:
return httpd.query_params['code']
else:
print 'Failed to find "code" in the query parameters of the redirect.'
print 'Try command-line authentication'
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()
print 'Go to the following link in your browser:'
print
print ' ' + authorize_url
print
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 CredentialsFileSymbolicLinkError:
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, 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:
print config
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, 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, e:
raise AuthenticationError('OAuth2 code exchange failed: %s' % e)
print 'Authentication successful.'
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)

View File

@ -0,0 +1,38 @@
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)

View File

@ -0,0 +1,322 @@
import io
import mimetypes
from apiclient import errors
from apiclient.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, 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, 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, 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, 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, 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

View File

@ -0,0 +1,193 @@
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), e:
print 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)

View File

@ -0,0 +1,7 @@
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"

View File

@ -4,10 +4,10 @@ import xbmcvfs
import xbmcgui
import zipfile
import zlib
import os
from dropbox import client, rest, session
APP_KEY = utils.getSetting('dropbox_key')
APP_SECRET = utils.getSetting('dropbox_secret')
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
class Vfs:
root_path = None
@ -38,9 +38,6 @@ class Vfs:
def put(self,source,dest):
return True
def getFile(self,source):
return True
def rmdir(self,directory):
return True
@ -116,19 +113,25 @@ class ZipFileSystem(Vfs):
class DropboxFileSystem(Vfs):
client = None
APP_KEY = ''
APP_SECRET = ''
def __init__(self,rootString):
self.set_root(rootString)
self.APP_KEY = utils.getSetting('dropbox_key')
self.APP_SECRET = utils.getSetting('dropbox_secret')
self.setup()
def setup(self):
if(APP_KEY == '' or APP_SECRET == ''):
if(self.APP_KEY == '' or self.APP_SECRET == ''):
xbmcgui.Dialog().ok(utils.getString(30010),utils.getString(30058),utils.getString(30059))
return
user_token_key,user_token_secret = self.getToken()
sess = session.DropboxSession(APP_KEY,APP_SECRET,"app_folder")
sess = session.DropboxSession(self.APP_KEY,self.APP_SECRET,"app_folder")
if(user_token_key == '' and user_token_secret == ''):
token = sess.obtain_request_token()
@ -161,9 +164,9 @@ class DropboxFileSystem(Vfs):
for aFile in metadata['contents']:
if(aFile['is_dir']):
dirs.append(aFile['path'][len(directory):])
dirs.append(utils.encode(aFile['path'][len(directory):]))
else:
files.append(aFile['path'][len(directory):])
files.append(utils.encode(aFile['path'][len(directory):]))
return [dirs,files]
else:
@ -270,6 +273,212 @@ class DropboxFileSystem(Vfs):
xbmcvfs.delete(xbmc.translatePath(utils.data_dir() + "tokens.txt"))
class GoogleDriveFilesystem(Vfs):
drive = None
history = {}
CLIENT_ID = ''
CLIENT_SECRET = ''
FOLDER_TYPE = 'application/vnd.google-apps.folder'
def __init__(self,rootString):
self.set_root(rootString)
self.CLIENT_ID = utils.getSetting('google_drive_id')
self.CLIENT_SECRET = utils.getSetting('google_drive_secret')
self.setup()
def setup(self):
#create authorization helper and load default settings
gauth = GoogleAuth(xbmc.validatePath(xbmc.translatePath(utils.addon_dir() + '/resources/lib/pydrive/settings.yaml')))
gauth.LoadClientConfigSettings()
#check if this user is already authorized
if(not xbmcvfs.exists(xbmc.translatePath(utils.data_dir() + "google_drive.dat"))):
settings = {"client_id":self.CLIENT_ID,'client_secret':self.CLIENT_SECRET}
drive_url = gauth.GetAuthUrl(settings)
utils.log("Google Drive Authorize URL: " + drive_url)
code = xbmcgui.Dialog().input('Google Drive Validation Code','Input the Validation code after authorizing this app')
gauth.Auth(code)
gauth.SaveCredentialsFile(xbmc.validatePath(xbmc.translatePath(utils.data_dir() + 'google_drive.dat')))
else:
gauth.LoadCredentialsFile(xbmc.validatePath(xbmc.translatePath(utils.data_dir() + 'google_drive.dat')))
#create the drive object
self.drive = GoogleDrive(gauth)
#make sure we have the folder we need
xbmc_folder = self._getGoogleFile(self.root_path)
print xbmc_folder
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

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<settings>
<category id="general" label="30011">
<setting id="remote_selection" type="enum" lvalues="30018|30019|30027" default="0" label="30025"/>
<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="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/remove_auth.py)" visible="eq(-5,2)"/>
<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="remove_auth_button" type="action" label="30093" action="RunScript(special://home/addons/script.xbmcbackup/remove_auth.py)" visible="gt(-7,1)"/>
<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" />