From b593a5512057dff68bebf6adec73a81e56370198 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 11:31:26 -0500 Subject: [PATCH 01/26] update to ignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index efed6ed..4509144 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyo +*.pyc .project .pydevproject \ No newline at end of file From 0df7364bea8ed4a66da1656ed2bb48320b5507db Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 11:31:38 -0500 Subject: [PATCH 02/26] added pydrive files --- resources/lib/pydrive/__init__.py | 0 resources/lib/pydrive/apiattr.py | 173 +++++++++++ resources/lib/pydrive/auth.py | 432 ++++++++++++++++++++++++++++ resources/lib/pydrive/drive.py | 38 +++ resources/lib/pydrive/files.py | 322 +++++++++++++++++++++ resources/lib/pydrive/settings.py | 190 ++++++++++++ resources/lib/pydrive/settings.yaml | 7 + 7 files changed, 1162 insertions(+) create mode 100644 resources/lib/pydrive/__init__.py create mode 100644 resources/lib/pydrive/apiattr.py create mode 100644 resources/lib/pydrive/auth.py create mode 100644 resources/lib/pydrive/drive.py create mode 100644 resources/lib/pydrive/files.py create mode 100644 resources/lib/pydrive/settings.py create mode 100644 resources/lib/pydrive/settings.yaml diff --git a/resources/lib/pydrive/__init__.py b/resources/lib/pydrive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/lib/pydrive/apiattr.py b/resources/lib/pydrive/apiattr.py new file mode 100644 index 0000000..891956d --- /dev/null +++ b/resources/lib/pydrive/apiattr.py @@ -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'] diff --git a/resources/lib/pydrive/auth.py b/resources/lib/pydrive/auth.py new file mode 100644 index 0000000..dbc0c51 --- /dev/null +++ b/resources/lib/pydrive/auth.py @@ -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) diff --git a/resources/lib/pydrive/drive.py b/resources/lib/pydrive/drive.py new file mode 100644 index 0000000..94233e4 --- /dev/null +++ b/resources/lib/pydrive/drive.py @@ -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) diff --git a/resources/lib/pydrive/files.py b/resources/lib/pydrive/files.py new file mode 100644 index 0000000..e60d010 --- /dev/null +++ b/resources/lib/pydrive/files.py @@ -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 diff --git a/resources/lib/pydrive/settings.py b/resources/lib/pydrive/settings.py new file mode 100644 index 0000000..fbfe4cf --- /dev/null +++ b/resources/lib/pydrive/settings.py @@ -0,0 +1,190 @@ +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 + }, + 'client_secret': { + 'type': str, + 'required': True + }, + '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: + 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) diff --git a/resources/lib/pydrive/settings.yaml b/resources/lib/pydrive/settings.yaml new file mode 100644 index 0000000..c28d08e --- /dev/null +++ b/resources/lib/pydrive/settings.yaml @@ -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" \ No newline at end of file From c36df5ade920a848cc9c077425e95b5a5ca99a96 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 11:32:15 -0500 Subject: [PATCH 03/26] pydrive license --- resources/lib/pydrive/LICENSE.txt | 185 ++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 resources/lib/pydrive/LICENSE.txt diff --git a/resources/lib/pydrive/LICENSE.txt b/resources/lib/pydrive/LICENSE.txt new file mode 100644 index 0000000..5c87881 --- /dev/null +++ b/resources/lib/pydrive/LICENSE.txt @@ -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. \ No newline at end of file From 74cb8c2b280b7af112038709c4c06d468623ace9 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 14:30:39 -0500 Subject: [PATCH 04/26] added google drive vfs --- resources/lib/pydrive/settings.py | 7 +- resources/lib/vfs.py | 209 ++++++++++++++++++++++++++++-- 2 files changed, 206 insertions(+), 10 deletions(-) diff --git a/resources/lib/pydrive/settings.py b/resources/lib/pydrive/settings.py index fbfe4cf..468e3cc 100644 --- a/resources/lib/pydrive/settings.py +++ b/resources/lib/pydrive/settings.py @@ -59,11 +59,13 @@ SETTINGS_STRUCT = { 'struct': { 'client_id': { 'type': str, - 'required': True + 'required': True, + 'default':'blank' }, 'client_secret': { 'type': str, - 'required': True + 'required': True, + 'default':'blank' }, 'auth_uri': { 'type': str, @@ -119,6 +121,7 @@ def LoadSettingsFile(filename=SETTINGS_FILE): stream = file(filename, 'r') data = load(stream, Loader=Loader) except (YAMLError, IOError), e: + print e raise SettingsError(e) return data diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index 3e9c6e3..670a1fb 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -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() @@ -269,6 +272,196 @@ 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) + + 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(directory + aFile['title']) + else: + files.append(directory + aFile['title']) + + + return [dirs,files] + + def mkdir(self,directory): + result = True + + if(not directory.startswith('/')): + directory = '/' + directory + + #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], 'parent':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): + + #get the id of this file + file = self._getGoogleFile(source) + + if(file != None): + file.GetContentFile(dest) + + 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 + utils.log(file) + #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[file] != None): + utils.log('used history') + 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 + From 06fbef26db40f8b40b28c90ff9bfb2ee964ec57a Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 14:34:26 -0500 Subject: [PATCH 05/26] depends on googleapi --- addon.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addon.xml b/addon.xml index 2e2d9cc..7a45891 100644 --- a/addon.xml +++ b/addon.xml @@ -3,6 +3,7 @@ name="XBMC Backup" version="0.5.8.4" provider-name="robweber"> + executable From 9c73b5b0b8018a82b21dd9a5b1bf68871972eab8 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 14:34:33 -0500 Subject: [PATCH 06/26] added google drive to settings --- resources/language/English/strings.xml | 1 + resources/settings.xml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 7ba8a6b..1eb796c 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -80,4 +80,5 @@ This version of XBMC is different than the one used to create the archive Compress Archives Copying Zip Archive + Google Drive diff --git a/resources/settings.xml b/resources/settings.xml index f3aaa6b..c2ee3fb 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,11 +1,13 @@ - + + + From e316e8201392eb75f5cf72b3dfc57baa5370f32b Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 14:40:05 -0500 Subject: [PATCH 07/26] create google file system --- resources/lib/backup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index 58de387..efce29c 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -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 def folderSort(aKey): result = aKey[0] @@ -54,6 +54,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 = '/' + self.remote_vfs = GoogleDriveFilesystem('/') def remoteConfigured(self): result = True @@ -423,8 +426,8 @@ class XbmcBackup: dest.mkdir(dest.root_path + aFile[len(source.root_path) + 1:]) else: self._updateProgress() - 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 source.get_file(aFile,dest.root_path + aFile[len(source.root_path):]) else: #copy using normal method From cf40edad792c5bdd3a55cbb2d760e9eed02e8cb6 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 14:42:59 -0500 Subject: [PATCH 08/26] use 'xbmc backup' as root directory --- resources/lib/backup.py | 4 ++-- resources/lib/vfs.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index efce29c..1c1e2b1 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -55,8 +55,8 @@ class XbmcBackup: self.remote_base_path = "/" self.remote_vfs = DropboxFileSystem("/") elif(utils.getSetting('remote_selection') == '3'): - self.remote_base_path = '/' - self.remote_vfs = GoogleDriveFilesystem('/') + self.remote_base_path = '/XBMC Backup/' + self.remote_vfs = GoogleDriveFilesystem('/XBMC Backup/') def remoteConfigured(self): result = True diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index 670a1fb..016154a 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -309,6 +309,13 @@ class GoogleDriveFilesystem(Vfs): #create the drive object self.drive = GoogleDrive(gauth) + + #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 = [] From e90c8e7803b7d39dd7f80614ef92f92ecb4fa4d5 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Thu, 9 Oct 2014 15:19:24 -0500 Subject: [PATCH 09/26] slight changes so mkdir works --- resources/lib/vfs.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index 016154a..a4c2e56 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -312,7 +312,7 @@ class GoogleDriveFilesystem(Vfs): #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) @@ -350,10 +350,14 @@ class GoogleDriveFilesystem(Vfs): 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() @@ -362,7 +366,7 @@ class GoogleDriveFilesystem(Vfs): parentFolder = self._getGoogleFile(pathList[0]) if(parentFolder != None): - newFolder = self.drive.CreateFile({'title': pathList[1], 'parent':parentFolder['id'],'mimeType':self.FOLDER_TYPE}) + newFolder = self.drive.CreateFile({'title': pathList[1],"parents":[{'kind':'drive#fileLink','id':parentFolder['id']}],'mimeType':self.FOLDER_TYPE}) newFolder.Upload() else: result = False @@ -431,7 +435,7 @@ class GoogleDriveFilesystem(Vfs): def _getGoogleFile(self,file): result = None - utils.log(file) + #file must start with / and not end with one (even directory) if(not file.startswith('/')): file = '/' + file @@ -439,8 +443,8 @@ class GoogleDriveFilesystem(Vfs): if(file.endswith('/')): file = file[:-1] - if(self.history[file] != None): - utils.log('used history') + if(self.history.has_key(file)): + result = self.history[file] else: pathList = os.path.split(file) @@ -457,7 +461,7 @@ class GoogleDriveFilesystem(Vfs): #recurse down the tree current_file = pathList[1] - parentId = self.getGoogleFile(pathList[0]) + parentId = self._getGoogleFile(pathList[0]) if(parentId != None): self.history[pathList[0]] = parentId From 9246c9b586742c327f2649d34ced7986aa50c81a Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 10 Oct 2014 08:49:21 -0500 Subject: [PATCH 10/26] vfs should return encoded dirs/files --- resources/lib/vfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index a4c2e56..edd3bf2 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -164,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: @@ -337,9 +337,9 @@ class GoogleDriveFilesystem(Vfs): for aFile in fileList: if(aFile['mimeType'] == self.FOLDER_TYPE): - dirs.append(directory + aFile['title']) + dirs.append(utils.encode(aFile['title'])) else: - files.append(directory + aFile['title']) + files.append(utils.encode(aFile['title'])) return [dirs,files] From 64ae75252f9ac9e6de8e66412686968c4da5fff6 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 10 Oct 2014 08:50:29 -0500 Subject: [PATCH 11/26] some spacing --- resources/lib/backup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index 1c1e2b1..eead643 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -71,7 +71,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")): @@ -458,8 +458,9 @@ class XbmcBackup: if(total_backups > 0): #get a list of valid backup folders dirs = self.listBackups() - + if(len(dirs) > total_backups): + #remove backups to equal total wanted remove_num = 0 self.filesTotal = self.filesTotal + remove_num + 1 From 69afda758ba7970e8ebd086763b2b8236285ee46 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Mon, 13 Oct 2014 14:33:47 -0500 Subject: [PATCH 12/26] updated readme for google drive --- README.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.txt b/README.txt index 11efe87..676d74f 100644 --- a/README.txt +++ b/README.txt @@ -36,6 +36,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. XBMC 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 XBMC 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 XBMC and proceed as normal. XBMC 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 similiar to the Dropbox one. You must create a Google API project and authenticate your account via the id and secret. Instructions for creating the Google API for Google Drive can be found here. You'll need the client id and client secret generated for the addon settings (https://developers.google.com/drive/web/quickstart/quickstart-python). 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 XBMC 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 authenication. From there the backup should start to run. Scripting XBMC Backup: From cdf67dbd0c1b25211f615850e45f188a522fbeb4 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Mon, 13 Oct 2014 14:35:21 -0500 Subject: [PATCH 13/26] spelling --- README.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.txt b/README.txt index 676d74f..cc83656 100644 --- a/README.txt +++ b/README.txt @@ -38,7 +38,7 @@ Once you have your app key and secret add them to the settings. XBMC Backup now Using Google Drive: -Using the Google Drive target is very similiar to the Dropbox one. You must create a Google API project and authenticate your account via the id and secret. Instructions for creating the Google API for Google Drive can be found here. You'll need the client id and client secret generated for the addon settings (https://developers.google.com/drive/web/quickstart/quickstart-python). You only need to follow Step 1. +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 creating the Google API for Google Drive can be found here. You'll need the client id and client secret generated for the addon settings (https://developers.google.com/drive/web/quickstart/quickstart-python). 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 XBMC 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 authenication. From there the backup should start to run. From fae5a052c749bd29a8ad061120615ff3f33554d1 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Wed, 5 Nov 2014 08:47:30 -0600 Subject: [PATCH 14/26] string id changed --- resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/settings.xml b/resources/settings.xml index c2ee3fb..4084a3f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,7 +1,7 @@ - + From 744ba772511ebdefe6eb45d20ecba33d81cd9e46 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Wed, 5 Nov 2014 08:47:47 -0600 Subject: [PATCH 15/26] updated for new file write checks --- resources/lib/backup.py | 4 ++-- resources/lib/vfs.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index e41053a..d074557 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -456,8 +456,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 diff --git a/resources/lib/vfs.py b/resources/lib/vfs.py index edd3bf2..eb57c63 100644 --- a/resources/lib/vfs.py +++ b/resources/lib/vfs.py @@ -396,12 +396,17 @@ class GoogleDriveFilesystem(Vfs): 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 From d68679f534a25751400260fb07fe3936166c4741 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Wed, 5 Nov 2014 09:48:59 -0600 Subject: [PATCH 16/26] updated dependencies --- addon.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 91c08f8..a2397b9 100644 --- a/addon.xml +++ b/addon.xml @@ -3,7 +3,11 @@ name="XBMC Backup" version="0.5.8.7" provider-name="robweber"> - + + + + + executable From 16f4d4073a0caefcaaeea1d97ea66bd2ecbb89df Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 21 Nov 2014 13:42:39 -0600 Subject: [PATCH 17/26] weird space we don't need --- resources/lib/backup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index b29b080..9405cb5 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -498,7 +498,6 @@ class XbmcBackup: dirs = self.listBackups() if(len(dirs) > total_backups): - #remove backups to equal total wanted remove_num = 0 self.filesTotal = self.filesTotal + remove_num + 1 From e395c60293a4fa4d3b3465c3dd743e850f3fb928 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 21 Nov 2014 13:48:11 -0600 Subject: [PATCH 18/26] removed xbmc reference --- README.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.txt b/README.txt index 2b7a62d..38b767a 100644 --- a/README.txt +++ b/README.txt @@ -51,7 +51,7 @@ 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 creating the Google API for Google Drive can be found here. You'll need the client id and client secret generated for the addon settings (https://developers.google.com/drive/web/quickstart/quickstart-python). 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 XBMC 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 authenication. From there the backup should start to run. +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: From f887df01972d467f18693c5e2c9501ba5e459f83 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 21 Nov 2014 14:17:48 -0600 Subject: [PATCH 19/26] folder should be Kodi Backup --- resources/lib/backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/backup.py b/resources/lib/backup.py index 9405cb5..8030da9 100644 --- a/resources/lib/backup.py +++ b/resources/lib/backup.py @@ -56,8 +56,8 @@ class XbmcBackup: self.remote_base_path = "/" self.remote_vfs = DropboxFileSystem("/") elif(utils.getSetting('remote_selection') == '3'): - self.remote_base_path = '/XBMC Backup/' - self.remote_vfs = GoogleDriveFilesystem('/XBMC Backup/') + self.remote_base_path = '/Kodi Backup/' + self.remote_vfs = GoogleDriveFilesystem('/Kodi Backup/') def remoteConfigured(self): result = True From 044cdc4bdc64d80941bff14698e0ab4706f5fc8e Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 13 Feb 2015 09:44:50 -0600 Subject: [PATCH 20/26] added google drive to removal list --- remove_auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/remove_auth.py b/remove_auth.py index b76d43d..f9ac08e 100644 --- a/remove_auth.py +++ b/remove_auth.py @@ -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 + From 5c40b0edc46033e8a6d1ef7a5a2e27eb59d371b2 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 13 Feb 2015 09:50:07 -0600 Subject: [PATCH 21/26] mixed up string ids --- resources/settings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/settings.xml b/resources/settings.xml index ae6ab87..5b98381 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,14 +1,14 @@ - + - + From 5050af931c444fec8ea282d7438bf70d55b7d7b2 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 13 Feb 2015 09:50:16 -0600 Subject: [PATCH 22/26] updated readme --- README.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.txt b/README.txt index 38b767a..c0cf55c 100644 --- a/README.txt +++ b/README.txt @@ -49,7 +49,7 @@ Once you have your app key and secret add them to the settings. Kodi Backup now 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 creating the Google API for Google Drive can be found here. You'll need the client id and client secret generated for the addon settings (https://developers.google.com/drive/web/quickstart/quickstart-python). You only need to follow Step 1. +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. From 1eb84c6ecf584fb5616aee0f5e1ea2a06820d77f Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 24 Apr 2015 09:29:26 -0500 Subject: [PATCH 23/26] added google drive string back in --- resources/language/English/strings.xml | 1 + resources/settings.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 1e72c8e..211588a 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -89,4 +89,5 @@ Do you want to do this? Old Zip Archive could not be deleted This needs to happen before a backup can run + Google Drive diff --git a/resources/settings.xml b/resources/settings.xml index 5b98381..66f3c2d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,7 +1,7 @@ - + From 1d7ca0afcf023b7e2c4e0382fcd204e419ee6fdd Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Fri, 24 Apr 2015 10:01:30 -0500 Subject: [PATCH 24/26] these appear to do nothing, deps are needed --- addon.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addon.xml b/addon.xml index 8e5a202..3e0d155 100644 --- a/addon.xml +++ b/addon.xml @@ -3,11 +3,11 @@ name="Backup" version="1.0.5" provider-name="robweber"> - - - - - + + + + + executable From 99e19b8fd58c67a3771cf42ed1d66e540b826f5b Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Mon, 27 Apr 2015 15:21:12 -0500 Subject: [PATCH 25/26] use this more restrictive scope --- resources/lib/pydrive/settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/pydrive/settings.yaml b/resources/lib/pydrive/settings.yaml index c28d08e..1b64f1b 100644 --- a/resources/lib/pydrive/settings.yaml +++ b/resources/lib/pydrive/settings.yaml @@ -4,4 +4,4 @@ client_config: client_secret: "blank" get_refresh_token: True oauth_scope: - - "https://www.googleapis.com/auth/drive" \ No newline at end of file + - "https://www.googleapis.com/auth/drive.file" \ No newline at end of file From 7448b6fea1dc5ede549ca0fcf0821bf5832a8e3e Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Mon, 27 Apr 2015 15:30:11 -0500 Subject: [PATCH 26/26] use most current versions of these --- addon.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 3e0d155..9156c5a 100644 --- a/addon.xml +++ b/addon.xml @@ -4,10 +4,10 @@ - + - + executable