2020-01-09 22:07:03 +01:00
|
|
|
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):
|
2023-10-01 15:37:20 +02:00
|
|
|
r"""
|
2020-01-09 22:07:03 +01:00
|
|
|
Google Drive plugin.
|
2023-10-01 15:37:20 +02:00
|
|
|
|
|
|
|
1. Create your Google application, if you don't have one already, on
|
|
|
|
the `developers console <https://console.developers.google.com>`_.
|
|
|
|
|
|
|
|
2. You may have to explicitly enable your user to use the app if the app
|
|
|
|
is created in test mode. Go to "OAuth consent screen" and add your user's
|
|
|
|
email address to the list of authorized users.
|
|
|
|
|
|
|
|
3. Select the scopes that you want to enable for your application, depending
|
|
|
|
on the integrations that you want to use.
|
|
|
|
See https://developers.google.com/identity/protocols/oauth2/scopes
|
|
|
|
for a list of the available scopes.
|
|
|
|
|
|
|
|
4. Click on "Credentials", then "Create credentials" -> "OAuth client ID".
|
|
|
|
|
|
|
|
5 Select "Desktop app", enter whichever name you like, and click "Create".
|
|
|
|
|
|
|
|
6. Click on the "Download JSON" icon next to your newly created client ID.
|
2024-01-18 23:46:30 +01:00
|
|
|
Save the JSON file under
|
|
|
|
``<WORKDIR>/credentials/google/client_secret.json``.
|
|
|
|
|
|
|
|
7.1. If you're running the service on a desktop environment, then you
|
|
|
|
can just start the application. A browser window will open and
|
|
|
|
you'll be asked to authorize the application - you may be prompted
|
|
|
|
with a warning because you are running a personal and potentially
|
|
|
|
unverified application. After authorizing the application, the
|
|
|
|
process will save the credentials under
|
|
|
|
``<WORKDIR>/credentials/google/<list,of,scopes>.json`` and proceed
|
|
|
|
with the plugin initialization.
|
|
|
|
|
|
|
|
7.2. If you're running the service on a headless environment, or you
|
|
|
|
prefer to manually generate the credentials file before copying to
|
|
|
|
another machine, you can run the following command:
|
|
|
|
|
|
|
|
.. code-block:: bash
|
|
|
|
|
|
|
|
mkdir -p <WORKDIR>/credentials/google
|
|
|
|
python -m platypush.plugins.google.credentials \
|
|
|
|
'drive,drive.appfolder,drive.photos.readonly' \
|
|
|
|
[--noauth_local_webserver] \
|
|
|
|
<WORKDIR>/credentials/google/client_secret.json
|
|
|
|
|
|
|
|
When launched with ``--noauth_local_webserver``, the script will
|
|
|
|
start a local webserver and print a URL that should be opened in
|
|
|
|
your browser. After authorizing the application, you may be
|
|
|
|
prompted with a code that you should copy and paste back to the
|
|
|
|
script. Otherwise, if you're running the script on a desktop, a
|
|
|
|
browser window will be opened automatically.
|
2023-10-01 15:37:20 +02:00
|
|
|
|
2020-01-09 22:07:03 +01:00
|
|
|
"""
|
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
scopes = [
|
|
|
|
'https://www.googleapis.com/auth/drive',
|
|
|
|
'https://www.googleapis.com/auth/drive.appfolder',
|
|
|
|
'https://www.googleapis.com/auth/drive.photos.readonly',
|
|
|
|
]
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(scopes=self.scopes, *args, **kwargs)
|
|
|
|
|
2023-10-01 15:37:20 +02:00
|
|
|
def get_service(self, **_):
|
2020-01-09 22:07:03 +01:00
|
|
|
return super().get_service(service='drive', version='v3')
|
|
|
|
|
|
|
|
# noinspection PyShadowingBuiltins
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
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]]:
|
2020-01-09 22:07:03 +01:00
|
|
|
"""
|
|
|
|
Get the list of files.
|
|
|
|
|
|
|
|
:param filter: Optional filter (default: None). See
|
2020-01-09 22:24:15 +01:00
|
|
|
`Google Drive API docs <https://developers.google.com/drive/api/v3/search-files>`_ for
|
2020-01-09 22:07:03 +01:00
|
|
|
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 += ' '
|
|
|
|
|
2023-10-01 15:37:20 +02:00
|
|
|
filter += f"'{folder_id}' in parents"
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
while True:
|
2023-09-24 16:54:43 +02:00
|
|
|
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()
|
|
|
|
)
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
page_token = results.get('nextPageToken')
|
2023-09-24 16:54:43 +02:00
|
|
|
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', [])
|
|
|
|
]
|
|
|
|
)
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
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
|
2023-09-24 16:54:43 +02:00
|
|
|
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:
|
2020-01-09 22:07:03 +01:00
|
|
|
"""
|
|
|
|
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.
|
2020-01-09 23:40:59 +01:00
|
|
|
:param description: File description.
|
2020-01-09 22:07:03 +01:00
|
|
|
: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 <https://developers.google.com/drive/api/v3/mime-types>`_ 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()
|
2023-09-24 16:54:43 +02:00
|
|
|
file = (
|
|
|
|
service.files()
|
|
|
|
.create(body=metadata, media_body=media, fields='*')
|
|
|
|
.execute()
|
|
|
|
)
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
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()
|
2023-10-01 15:37:20 +02:00
|
|
|
self.logger.info('Download progress: %s%%', status.progress())
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
with open(path, 'wb') as f:
|
|
|
|
f.write(fh.getbuffer().tobytes())
|
|
|
|
return path
|
|
|
|
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
def create(
|
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
description: Optional[str] = None,
|
|
|
|
mime_type: Optional[str] = None,
|
|
|
|
parents: Optional[List[str]] = None,
|
|
|
|
starred: bool = False,
|
|
|
|
) -> GoogleDriveFile:
|
2020-01-09 22:07:03 +01:00
|
|
|
"""
|
|
|
|
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()
|
2023-09-24 16:54:43 +02:00
|
|
|
file = service.files().create(body=metadata, fields='*').execute()
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
return GoogleDriveFile(
|
|
|
|
type=file.get('kind').split('#')[1],
|
|
|
|
id=file.get('id'),
|
|
|
|
name=file.get('name'),
|
|
|
|
mime_type=file.get('mimeType'),
|
|
|
|
)
|
|
|
|
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
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,
|
2023-10-01 15:37:20 +02:00
|
|
|
starred: Optional[bool] = None,
|
|
|
|
trashed: Optional[bool] = None,
|
2023-09-24 16:54:43 +02:00
|
|
|
) -> GoogleDriveFile:
|
2020-01-09 22:07:03 +01:00
|
|
|
"""
|
|
|
|
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()
|
2023-09-24 16:54:43 +02:00
|
|
|
file = (
|
|
|
|
service.files().update(fileId=file_id, body=metadata, fields='*').execute()
|
|
|
|
)
|
2020-01-09 22:07:03 +01:00
|
|
|
|
|
|
|
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:
|