diff --git a/pydrive2/auth.py b/pydrive2/auth.py index fa13f411..fc0331cc 100644 --- a/pydrive2/auth.py +++ b/pydrive2/auth.py @@ -2,6 +2,8 @@ import json import google.oauth2.credentials import google.oauth2.service_account +import threading +from functools import wraps from googleapiclient.discovery import build from pydrive2.storage import FileBackend @@ -42,6 +44,20 @@ 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): + # Initialize auth if needed. + if self.auth is None: + self.auth = GoogleAuth() + + return decoratee(self, *args, **kwargs) + + return _decorated + + class GoogleAuth(ApiAttributeMixin): """Wrapper class for oauth2client library in google-api-python-client. @@ -78,6 +94,7 @@ def __init__( """ self.http_timeout = http_timeout ApiAttributeMixin.__init__(self) + self.thread_local = threading.local() if settings is None and settings_file: try: @@ -88,12 +105,12 @@ def __init__( self.settings = settings or self.DEFAULT_SETTINGS ValidateSettings(self.settings) - self._storage = None self._service = None - self._credentials = None self._client_config = None self._oauth_type = None self._flow = None + self._storage = None + self._credentials = None # Lazy loading, read-only properties @property @@ -153,6 +170,18 @@ def credentials(self): return self._credentials + @property + def authorized_http(self): + # Ensure that a thread-safe, Authorized HTTP object is provided + # If HTTP object not specified, create or resuse an HTTP + # object from the thread local storage. + if not getattr(self.thread_local, "http", None): + self.thread_local.http = AuthorizedHttp( + self.credentials, http=self._build_http() + ) + + return self.thread_local.http + # Other properties @property def access_token_expired(self): @@ -612,9 +641,10 @@ def Authorize(self): ) def Get_Http_Object(self): - """Create and authorize an httplib2.Http object. Necessary for - thread-safety. + """Alias for self.authorized_http. To avoid creating multiple Http Objects by caching it per thread. :return: The http object to be used in each call. :rtype: httplib2.Http """ - return AuthorizedHttp(self.credentials, http=self._build_http()) + + # updated as alias for + return self.authorized_http diff --git a/pydrive2/drive.py b/pydrive2/drive.py index 83ad3916..b70462f4 100644 --- a/pydrive2/drive.py +++ b/pydrive2/drive.py @@ -1,6 +1,7 @@ from .apiattr import ApiAttributeMixin from .files import GoogleDriveFile from .files import GoogleDriveFileList +from .auth import LoadAuth class GoogleDrive(ApiAttributeMixin): @@ -39,6 +40,7 @@ def ListFile(self, param=None): """ return GoogleDriveFileList(auth=self.auth, param=param) + @LoadAuth def GetAbout(self): """Return information about the Google Drive of the auth instance. diff --git a/pydrive2/files.py b/pydrive2/files.py index ab26c72f..abf4724b 100644 --- a/pydrive2/files.py +++ b/pydrive2/files.py @@ -12,6 +12,7 @@ from .apiattr import ApiAttributeMixin from .apiattr import ApiResource from .apiattr import ApiResourceList +from .auth import LoadAuth BLOCK_SIZE = 1024 # Usage: MIME_TYPE_TO_BOM['']['']. @@ -67,6 +68,7 @@ def __init__(self, auth=None, param=None): """Create an instance of GoogleDriveFileList.""" super().__init__(auth=auth, metadata=param) + @LoadAuth def _GetList(self): """Overwritten method which actually makes API call to list files. @@ -285,6 +287,7 @@ def GetContentString( self.FetchContent(mimetype, remove_bom) return self.content.getvalue().decode(encoding) + @LoadAuth def GetContentFile( self, filename, @@ -352,6 +355,7 @@ def download(fd, request): if bom: self._RemovePrefix(fd, bom) + @LoadAuth def GetContentIOBuffer( self, mimetype=None, @@ -408,6 +412,7 @@ def GetContentIOBuffer( chunksize=chunksize, ) + @LoadAuth def FetchMetadata(self, fields=None, fetch_all=False): """Download file's metadata from id using Files.get(). @@ -547,6 +552,7 @@ def InsertPermission(self, new_permission, param=None): return permission + @LoadAuth def GetPermissions(self): """Get file's or shared drive's permissions. @@ -597,6 +603,7 @@ def _WrapRequest(self, request): request.http = self.auth.Get_Http_Object() return request + @LoadAuth def _FilesInsert(self, param=None): """Upload a new file using Files.insert(). @@ -626,6 +633,7 @@ def _FilesInsert(self, param=None): self.dirty["content"] = False self.UpdateMetadata(metadata) + @LoadAuth def _FilesUnTrash(self, param=None): """Un-delete (Trash) a file using Files.UnTrash(). :param param: additional parameter to file. @@ -650,6 +658,7 @@ def _FilesUnTrash(self, param=None): self.metadata["labels"]["trashed"] = False return True + @LoadAuth def _FilesTrash(self, param=None): """Soft-delete (Trash) a file using Files.Trash(). @@ -675,6 +684,7 @@ def _FilesTrash(self, param=None): self.metadata["labels"]["trashed"] = True return True + @LoadAuth def _FilesDelete(self, param=None): """Delete a file using Files.Delete() (WARNING: deleting permanently deletes the file!) @@ -699,6 +709,7 @@ def _FilesDelete(self, param=None): else: return True + @LoadAuth @LoadMetadata def _FilesUpdate(self, param=None): """Update metadata and/or content using Files.Update(). @@ -730,6 +741,7 @@ def _FilesUpdate(self, param=None): self.dirty["content"] = False self.UpdateMetadata(metadata) + @LoadAuth @LoadMetadata def _FilesPatch(self, param=None): """Update metadata using Files.Patch(). @@ -770,6 +782,7 @@ def _BuildMediaBody(self): self.content, self["mimeType"], resumable=True ) + @LoadAuth def _DownloadFromUrl(self, url): """Download file from url using provided credential. @@ -784,6 +797,7 @@ def _DownloadFromUrl(self, url): raise ApiRequestError(errors.HttpError(resp, content, uri=url)) return content + @LoadAuth def _DeletePermission(self, permission_id): """Deletes the permission remotely, and from the file object itself.