From e709908d7e19a191d7b5b20970416e0da028df65 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 9 Jan 2020 22:07:03 +0100 Subject: [PATCH] Added Google Drive plugin - closes #91 --- platypush/message/response/google/__init__.py | 0 platypush/message/response/google/drive.py | 23 ++ platypush/plugins/google/__init__.py | 2 + platypush/plugins/google/drive.py | 336 ++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 platypush/message/response/google/__init__.py create mode 100644 platypush/message/response/google/drive.py create mode 100644 platypush/plugins/google/drive.py diff --git a/platypush/message/response/google/__init__.py b/platypush/message/response/google/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/platypush/message/response/google/drive.py b/platypush/message/response/google/drive.py new file mode 100644 index 000000000..f164edf88 --- /dev/null +++ b/platypush/message/response/google/drive.py @@ -0,0 +1,23 @@ +from typing import Optional + +from platypush.message import Mapping +from platypush.message.response import Response + + +class GoogleDriveResponse(Response): + pass + + +class GoogleDriveFile(Mapping): + # noinspection PyShadowingBuiltins + def __init__(self, + type: str, + id: str, + name: str, + mime_type: Optional[str] = None, + *args, **kwargs): + super().__init__(id=id, name=name, type=type, + mime_type=mime_type, *args, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/google/__init__.py b/platypush/plugins/google/__init__.py index d0c7834e8..0475d9721 100644 --- a/platypush/plugins/google/__init__.py +++ b/platypush/plugins/google/__init__.py @@ -30,6 +30,8 @@ class GooglePlugin(Plugin): Requires: * **google-api-python-client** (``pip install google-api-python-client``) + * **oauth2client** (``pip install oauth2client``) + """ def __init__(self, scopes=None, *args, **kwargs): diff --git a/platypush/plugins/google/drive.py b/platypush/plugins/google/drive.py new file mode 100644 index 000000000..4827ae054 --- /dev/null +++ b/platypush/plugins/google/drive.py @@ -0,0 +1,336 @@ +import io +import os +from typing import Optional, List, Union + +from platypush.plugins import action +from platypush.plugins.google import GooglePlugin +from platypush.message.response.google.drive import GoogleDriveFile + + +class GoogleDrivePlugin(GooglePlugin): + """ + Google Drive plugin. + """ + + scopes = ['https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.appfolder', + 'https://www.googleapis.com/auth/drive.photos.readonly'] + + def __init__(self, *args, **kwargs): + super().__init__(scopes=self.scopes, *args, **kwargs) + + def get_service(self, **kwargs): + return super().get_service(service='drive', version='v3') + + # noinspection PyShadowingBuiltins + @action + def files(self, + filter: Optional[str] = None, + folder_id: Optional[str] = None, + limit: Optional[int] = 100, + drive_id: Optional[str] = None, + spaces: Optional[Union[str, List[str]]] = None, + order_by: Optional[Union[str, List[str]]] = None) -> Union[GoogleDriveFile, List[GoogleDriveFile]]: + """ + Get the list of files. + + :param filter: Optional filter (default: None). See + `Google Drive API docs ` for + the supported syntax. + + :param folder_id: Drive folder ID to search (default: get all files). + :param limit: Maximum number of entries to be retrieves (default: 100). + :param drive_id: Shared drive ID to search (default: None). + :param spaces: Drive spaces to search. Supported values: + + - ``drive`` + - ``appDataFolder`` + - ``photos`` + + :param order_by: Order the results by a specific attribute or list + of attributes (default: None). Supported attributes: + + - ``createdTime`` + - ``folder`` + - ``modifiedByMeTime`` + - ``modifiedTime`` + - ``name`` + - ``name_natural`` + - ``quotaBytesUsed`` + - ``recency`` + - ``sharedWithMeTime`` + - ``starred`` + - ``viewedByMeTime`` + + Attributes will be sorted in ascending order by default. You can change that by + by appending "``desc``" separated by a space to the attribute you want in descending + order - e.g. ``["folder", "createdTime desc", "modifiedTime desc"]``. + """ + + service = self.get_service() + page_token = None + files = [] + + if isinstance(order_by, list): + order_by = ','.join(order_by) + if isinstance(spaces, list): + spaces = ','.join(spaces) + if folder_id: + if not filter: + filter = '' + else: + filter += ' ' + + filter += "'{}' in parents".format(folder_id) + + while True: + results = service.files().list( + q=filter, + driveId=drive_id, + pageSize=limit, + orderBy=order_by, + fields="nextPageToken, files(id, name, kind, mimeType)", + pageToken=page_token, + spaces=spaces, + ).execute() + + page_token = results.get('nextPageToken') + files.extend([ + GoogleDriveFile( + id=f.get('id'), + name=f.get('name'), + type=f.get('kind').split('#')[1], + mime_type=f.get('mimeType'), + ) for f in results.get('files', []) + ]) + + if not page_token or (limit and len(files) >= limit): + break + + return files + + @action + def get(self, file_id: str): + """ + Get the information of a file on the Drive by file ID. + :param file_id: File ID. + """ + service = self.get_service() + file = service.files().get(fileId=file_id).execute() + return GoogleDriveFile( + type=file.get('kind').split('#')[1], + id=file.get('id'), + name=file.get('name'), + mime_type=file.get('mimeType'), + ) + + @action + def upload(self, + path: str, + mime_type: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + parents: Optional[List[str]] = None, + starred: bool = False, + target_mime_type: Optional[str] = None) -> GoogleDriveFile: + """ + Upload a file to Google Drive. + + :param path: Path of the file to upload. + :param mime_type: MIME type of the source file (e.g. "``image/jpeg``"). + :param name: Name of the target file. Default: same name as the source file. + :param description; File description. + :param parents: List of folder IDs that will contain the file (default: drive root). + :param starred: If True, then the uploaded file will be marked as starred by the user. + :param target_mime_type: Target MIME type. Useful if you want to e.g. import a CSV file as a Google Sheet + (use "``application/vnd.google-apps.spreadsheet``), or an ODT file to a Google Doc + (use "``application/vnd.google-apps.document``). See + `the official documentation `_ for a complete list + of supported types. + """ + # noinspection PyPackageRequirements + from googleapiclient.http import MediaFileUpload + + path = os.path.abspath(os.path.expanduser(path)) + name = name or os.path.basename(path) + metadata = { + 'name': name, + 'description': description, + 'parents': parents, + 'starred': starred, + } + + if target_mime_type: + metadata['mimeType'] = target_mime_type + + media = MediaFileUpload(path, mimetype=mime_type) + service = self.get_service() + file = service.files().create( + body=metadata, + media_body=media, + fields='*' + ).execute() + + return GoogleDriveFile( + type=file.get('kind').split('#')[1], + id=file.get('id'), + name=file.get('name'), + mime_type=file.get('mimeType'), + ) + + @action + def download(self, file_id: str, path: str) -> str: + """ + Download a Google Drive file locally. + + :param file_id: Path of the file to upload. + :param path: Path of the file to upload. + :return: The local file path. + """ + # noinspection PyPackageRequirements + from googleapiclient.http import MediaIoBaseDownload + + service = self.get_service() + request = service.files().get_media(fileId=file_id) + path = os.path.abspath(os.path.expanduser(path)) + if os.path.isdir(path): + name = service.files().get(fileId=file_id).execute().get('name') + path = os.path.join(path, name) + + fh = io.BytesIO() + downloader = MediaIoBaseDownload(fh, request) + done = False + + while not done: + status, done = downloader.next_chunk() + self.logger.info('Download progress: {}%'.format(status.progress())) + + with open(path, 'wb') as f: + f.write(fh.getbuffer().tobytes()) + return path + + @action + def create(self, + name: str, + description: Optional[str] = None, + mime_type: Optional[str] = None, + parents: Optional[List[str]] = None, + starred: bool = False) -> GoogleDriveFile: + """ + Create a file. + + :param name: File name. + :param description: File description. + :param mime_type: File MIME type. + :param parents: List of folder IDs that will contain the file (default: drive root). + :param starred: If True then the file will be marked as starred. + """ + metadata = { + 'name': name, + 'description': description, + 'parents': parents, + 'starred': starred, + } + + if mime_type: + metadata['mimeType'] = mime_type + + service = self.get_service() + file = service.files().create( + body=metadata, + fields='*' + ).execute() + + return GoogleDriveFile( + type=file.get('kind').split('#')[1], + id=file.get('id'), + name=file.get('name'), + mime_type=file.get('mimeType'), + ) + + @action + def update(self, + file_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + add_parents: Optional[List[str]] = None, + remove_parents: Optional[List[str]] = None, + mime_type: Optional[str] = None, + starred: bool = None, + trashed: bool = None) -> GoogleDriveFile: + """ + Update the metadata or the content of a file. + + :param file_id: File ID. + :param name: Set the file name. + :param description: Set the file description. + :param add_parents: Add the file to these parent folder IDs. + :param remove_parents: Remove the file from these parent folder IDs. + :param mime_type: Set the file MIME type. + :param starred: Change the starred flag. + :param trashed: Move/remove from trash. + """ + metadata = {} + if name: + metadata['name'] = name + if description is not None: + metadata['description'] = description + if add_parents: + metadata['add_parents'] = add_parents + if remove_parents: + metadata['remove_parents'] = remove_parents + if mime_type: + metadata['mimeType'] = mime_type + if starred is not None: + metadata['starred'] = starred + if trashed is not None: + metadata['trashed'] = trashed + + service = self.get_service() + file = service.files().update( + fileId=file_id, + body=metadata, + fields='*' + ).execute() + + return GoogleDriveFile( + type=file.get('kind').split('#')[1], + id=file.get('id'), + name=file.get('name'), + mime_type=file.get('mimeType'), + ) + + @action + def delete(self, file_id: str): + """ + Delete a file from Google Drive. + :param file_id: File ID. + """ + service = self.get_service() + service.files().delete(fileId=file_id).execute() + + @action + def copy(self, file_id: str) -> GoogleDriveFile: + """ + Create a copy of a file. + :param file_id: File ID. + """ + service = self.get_service() + file = service.files().copy(fileId=file_id).execute() + return GoogleDriveFile( + type=file.get('kind').split('#')[1], + id=file.get('id'), + name=file.get('name'), + mime_type=file.get('mimeType'), + ) + + @action + def empty_trash(self): + """ + Empty the Drive bin. + """ + service = self.get_service() + service.files().emptyTrash() + + +# vim:sw=4:ts=4:et: