Skip to content

Commit 7ad7d93

Browse files
committed
Add support for service account
Adding support service accounts Signed-off-by: lzzy12 <[email protected]> Added scripts and docs for generating service accounts Signed-off-by: lzzy12 <[email protected]> gen_sa_accounts: Save credentials with indexed file name Signed-off-by: lzzy12 <[email protected]> gdriveTools: Avoid using oauth2 library for service accounts oauth2 library is deprecated Signed-off-by: lzzy12 <[email protected]>
1 parent 997e572 commit 7ad7d93

File tree

7 files changed

+582
-52
lines changed

7 files changed

+582
-52
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ data*
1010
*.pickle
1111
authorized_chats.txt
1212
log.txt
13+
accounts/*

README.md

+31
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,34 @@ sudo docker build . -t mirror-bot
8787
```
8888
sudo docker run mirror-bot
8989
```
90+
91+
## Using service accounts for uploading to avoid user rate limit
92+
93+
Many thanks to [AutoRClone](https://github.com/xyou365/AutoRclone) for the scripts
94+
### Generating service accounts
95+
Step 1. Generate service accounts [What is service account](https://cloud.google.com/iam/docs/service-accounts) [How to use service account in rclone](https://rclone.org/drive/#service-account-support).
96+
---------------------------------
97+
Let us create only the service accounts that we need.
98+
**Warning:** abuse of this feature is not the aim of autorclone and we do **NOT** recommend that you make a lot of projects, just one project and 100 sa allow you plenty of use, its also possible that overabuse might get your projects banned by google.
99+
100+
```
101+
Note: 1 service account can copy around 750gb a day, 1 project makes 100 service accounts so thats 75tb a day, for most users this should easily suffice.
102+
```
103+
104+
`python3 gen_sa_accounts.py --quick-setup 1 --new-only`
105+
106+
A folder named accounts will be created which will contain keys for the service accounts created
107+
```
108+
We highly recommend to zip this folder and store it somewhere safe, so that you do not have to create a new project everytime you want to deploy the bot
109+
```
110+
### Adding service accounts to Google Groups:
111+
We use Google Groups to manager our service accounts considering the
112+
[Official limits to the members of Team Drive](https://support.google.com/a/answer/7338880?hl=en) (Limit for individuals and groups directly added as members: 600).
113+
114+
1. Turn on the Directory API following [official steps](https://developers.google.com/admin-sdk/directory/v1/quickstart/python) (save the generated json file to folder `credentials`).
115+
116+
2. Create group for your organization [in the Admin console](https://support.google.com/a/answer/33343?hl=en). After create a group, you will have an address for example`[email protected]`.
117+
118+
3. Run `python3 add_to_google_group.py -g [email protected]`
119+
120+
4. Now, add Google Groups (**Step 2**) to manager your service accounts, add the group address `[email protected]` or `[email protected]` to the Team drive or folder

add_to_google_group.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# auto rclone
2+
# Add service accounts to groups for your organization
3+
#
4+
# Author Telegram https://t.me/CodyDoby
5+
6+
7+
from __future__ import print_function
8+
9+
import os
10+
import pickle
11+
12+
import argparse
13+
import glob
14+
import googleapiclient.discovery
15+
import json
16+
import progress.bar
17+
import time
18+
from google.auth.transport.requests import Request
19+
from google_auth_oauthlib.flow import InstalledAppFlow
20+
21+
stt = time.time()
22+
23+
parse = argparse.ArgumentParser(
24+
description='A tool to add service accounts to groups for your organization from a folder containing credential '
25+
'files.')
26+
parse.add_argument('--path', '-p', default='accounts',
27+
help='Specify an alternative path to the service accounts folder.')
28+
parse.add_argument('--credentials', '-c', default='credentials/credentials.json',
29+
help='Specify the relative path for the controller file.')
30+
parsereq = parse.add_argument_group('required arguments')
31+
32+
parsereq.add_argument('--groupaddr', '-g', help='The address of groups for your organization.', required=True)
33+
34+
args = parse.parse_args()
35+
acc_dir = args.path
36+
gaddr = args.groupaddr
37+
credentials = glob.glob(args.credentials)
38+
39+
creds = None
40+
if os.path.exists('credentials/token.pickle'):
41+
with open('credentials/token.pickle', 'rb') as token:
42+
creds = pickle.load(token)
43+
# If there are no (valid) credentials available, let the user log in.
44+
if not creds or not creds.valid:
45+
if creds and creds.expired and creds.refresh_token:
46+
creds.refresh(Request())
47+
else:
48+
flow = InstalledAppFlow.from_client_secrets_file(credentials[0], scopes=[
49+
'https://www.googleapis.com/auth/admin.directory.group',
50+
'https://www.googleapis.com/auth/admin.directory.group.member'
51+
])
52+
# creds = flow.run_local_server(port=0)
53+
creds = flow.run_console()
54+
# Save the credentials for the next run
55+
with open('credentials/token.pickle', 'wb') as token:
56+
pickle.dump(creds, token)
57+
58+
group = googleapiclient.discovery.build("admin", "directory_v1", credentials=creds)
59+
60+
print(group.members())
61+
62+
batch = group.new_batch_http_request()
63+
64+
sa = glob.glob('%s/*.json' % acc_dir)
65+
66+
# sa = sa[0:5]
67+
68+
pbar = progress.bar.Bar("Readying accounts", max=len(sa))
69+
for i in sa:
70+
ce = json.loads(open(i, 'r').read())['client_email']
71+
72+
body = {"email": ce, "role": "MEMBER"}
73+
batch.add(group.members().insert(groupKey=gaddr, body=body))
74+
# group.members().insert(groupKey=gaddr, body=body).execute()
75+
76+
pbar.next()
77+
pbar.finish()
78+
print('Adding...')
79+
batch.execute()
80+
81+
print('Complete.')
82+
hours, rem = divmod((time.time() - stt), 3600)
83+
minutes, sec = divmod(rem, 60)
84+
print("Elapsed Time:\n{:0>2}:{:0>2}:{:05.2f}".format(int(hours), int(minutes), sec))

bot/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ def getConfig(name: str):
9090
IS_TEAM_DRIVE = False
9191
except KeyError:
9292
IS_TEAM_DRIVE = False
93+
94+
try:
95+
USE_SERVICE_ACCOUNTS = getConfig('USE_SERVICE_ACCOUNTS')
96+
if USE_SERVICE_ACCOUNTS.lower() == 'true':
97+
USE_SERVICE_ACCOUNTS = True
98+
else:
99+
USE_SERVICE_ACCOUNTS = False
100+
except KeyError:
101+
USE_SERVICE_ACCOUNTS = False
102+
93103
updater = tg.Updater(token=BOT_TOKEN)
94104
bot = updater.bot
95-
dispatcher = updater.dispatcher
105+
dispatcher = updater.dispatcher

bot/helper/mirror_utils/upload_utils/gdriveTools.py

+88-50
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,66 @@
44
from urllib.parse import parse_qs
55

66
import requests
7+
78
from google.auth.transport.requests import Request
9+
from google.oauth2 import service_account
810
from google_auth_oauthlib.flow import InstalledAppFlow
911
from googleapiclient.discovery import build
1012
from googleapiclient.errors import HttpError
1113
from googleapiclient.http import MediaFileUpload
1214
from tenacity import *
1315

14-
from bot import LOGGER, parent_id, DOWNLOAD_DIR, IS_TEAM_DRIVE, INDEX_URL
16+
from bot import LOGGER, parent_id, DOWNLOAD_DIR, IS_TEAM_DRIVE, INDEX_URL, DOWNLOAD_STATUS_UPDATE_INTERVAL, \
17+
USE_SERVICE_ACCOUNTS
1518
from bot.helper.ext_utils.bot_utils import *
1619
from bot.helper.ext_utils.fs_utils import get_mime_type
1720

1821
logging.getLogger('googleapiclient.discovery').setLevel(logging.ERROR)
1922

23+
G_DRIVE_TOKEN_FILE = "token.pickle"
24+
# Check https://developers.google.com/drive/scopes for all available scopes
25+
OAUTH_SCOPE = ["https://www.googleapis.com/auth/drive"]
26+
27+
SERVICE_ACCOUNT_INDEX = 0
28+
29+
30+
def authorize():
31+
# Get credentials
32+
credentials = None
33+
if not USE_SERVICE_ACCOUNTS:
34+
if os.path.exists(G_DRIVE_TOKEN_FILE):
35+
with open(G_DRIVE_TOKEN_FILE, 'rb') as f:
36+
credentials = pickle.load(f)
37+
if credentials is None or not credentials.valid:
38+
if credentials and credentials.expired and credentials.refresh_token:
39+
credentials.refresh(Request())
40+
else:
41+
flow = InstalledAppFlow.from_client_secrets_file(
42+
'credentials.json', OAUTH_SCOPE)
43+
LOGGER.info(flow)
44+
credentials = flow.run_console(port=0)
45+
46+
# Save the credentials for the next run
47+
with open(G_DRIVE_TOKEN_FILE, 'wb') as token:
48+
pickle.dump(credentials, token)
49+
else:
50+
credentials = service_account.Credentials \
51+
.from_service_account_file(f'accounts/{SERVICE_ACCOUNT_INDEX}.json',
52+
scopes=OAUTH_SCOPE)
53+
return build('drive', 'v3', credentials=credentials, cache_discovery=False)
54+
55+
56+
service = authorize()
57+
2058

2159
class GoogleDriveHelper:
60+
# Redirect URI for installed apps, can be left as is
61+
REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
62+
G_DRIVE_DIR_MIME_TYPE = "application/vnd.google-apps.folder"
63+
G_DRIVE_BASE_DOWNLOAD_URL = "https://drive.google.com/uc?id={}&export=download"
2264

2365
def __init__(self, name=None, listener=None):
24-
self.__G_DRIVE_TOKEN_FILE = "token.pickle"
25-
# Check https://developers.google.com/drive/scopes for all available scopes
26-
self.__OAUTH_SCOPE = ["https://www.googleapis.com/auth/drive"]
27-
# Redirect URI for installed apps, can be left as is
28-
self.__REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
29-
self.__G_DRIVE_DIR_MIME_TYPE = "application/vnd.google-apps.folder"
30-
self.__G_DRIVE_BASE_DOWNLOAD_URL = "https://drive.google.com/uc?id={}&export=download"
31-
self.__G_DRIVE_DIR_BASE_DOWNLOAD_URL = "https://drive.google.com/drive/folders/{}"
3266
self.__listener = listener
33-
self.__service = self.authorize()
3467
self._file_uploaded_bytes = 0
3568
self.uploaded_bytes = 0
3669
self.start_time = 0
@@ -74,6 +107,21 @@ def _on_upload_progress(self):
74107
self.uploaded_bytes += chunk_size
75108
self.total_time += self.update_interval
76109

110+
@staticmethod
111+
def __upload_empty_file(path, file_name, mime_type, parent_id=None):
112+
media_body = MediaFileUpload(path,
113+
mimetype=mime_type,
114+
resumable=False)
115+
file_metadata = {
116+
'name': file_name,
117+
'description': 'mirror',
118+
'mimeType': mime_type,
119+
}
120+
if parent_id is not None:
121+
file_metadata['parents'] = [parent_id]
122+
return service.files().create(supportsTeamDrives=True,
123+
body=file_metadata, media_body=media_body).execute()
124+
77125
@retry(wait=wait_exponential(multiplier=2, min=3, max=6), stop=stop_after_attempt(5),
78126
retry=retry_if_exception_type(HttpError), before=before_log(LOGGER, logging.DEBUG))
79127
def __set_permission(self, drive_id):
@@ -83,11 +131,13 @@ def __set_permission(self, drive_id):
83131
'value': None,
84132
'withLink': True
85133
}
86-
return self.__service.permissions().create(supportsTeamDrives=True, fileId=drive_id, body=permissions).execute()
134+
return service.permissions().create(supportsTeamDrives=True, fileId=drive_id, body=permissions).execute()
87135

88136
@retry(wait=wait_exponential(multiplier=2, min=3, max=6), stop=stop_after_attempt(5),
89137
retry=retry_if_exception_type(HttpError), before=before_log(LOGGER, logging.DEBUG))
90138
def upload_file(self, file_path, file_name, mime_type, parent_id):
139+
global SERVICE_ACCOUNT_INDEX
140+
global service
91141
# File body description
92142
file_metadata = {
93143
'name': file_name,
@@ -101,34 +151,42 @@ def upload_file(self, file_path, file_name, mime_type, parent_id):
101151
media_body = MediaFileUpload(file_path,
102152
mimetype=mime_type,
103153
resumable=False)
104-
response = self.__service.files().create(supportsTeamDrives=True,
105-
body=file_metadata, media_body=media_body).execute()
154+
response = service.files().create(supportsTeamDrives=True,
155+
body=file_metadata, media_body=media_body).execute()
106156
if not IS_TEAM_DRIVE:
107157
self.__set_permission(response['id'])
108-
drive_file = self.__service.files().get(supportsTeamDrives=True,
109-
fileId=response['id']).execute()
110-
download_url = self.__G_DRIVE_BASE_DOWNLOAD_URL.format(drive_file.get('id'))
158+
drive_file = service.files().get(supportsTeamDrives=True,
159+
fileId=response['id']).execute()
160+
download_url = self.G_DRIVE_BASE_DOWNLOAD_URL.format(drive_file.get('id'))
111161
return download_url
112162
media_body = MediaFileUpload(file_path,
113163
mimetype=mime_type,
114164
resumable=True,
115165
chunksize=50 * 1024 * 1024)
116166

117167
# Insert a file
118-
drive_file = self.__service.files().create(supportsTeamDrives=True,
119-
body=file_metadata, media_body=media_body)
168+
drive_file = service.files().create(supportsTeamDrives=True,
169+
body=file_metadata, media_body=media_body)
120170
response = None
121171
while response is None:
122172
if self.is_cancelled:
123173
return None
124-
self.status, response = drive_file.next_chunk()
174+
try:
175+
self.status, response = drive_file.next_chunk()
176+
except HttpError as err:
177+
if err.resp.get('content-type', '').startswith('application/json'):
178+
reason = json.loads(err.content).get('error').get('errors')[0].get('reason')
179+
if reason == 'userRateLimitExceeded':
180+
SERVICE_ACCOUNT_INDEX += 1
181+
service = authorize()
182+
raise err
125183
self._file_uploaded_bytes = 0
126184
# Insert new permissions
127185
if not IS_TEAM_DRIVE:
128186
self.__set_permission(response['id'])
129187
# Define file instance and get url for download
130-
drive_file = self.__service.files().get(supportsTeamDrives=True, fileId=response['id']).execute()
131-
download_url = self.__G_DRIVE_BASE_DOWNLOAD_URL.format(drive_file.get('id'))
188+
drive_file = service.files().get(supportsTeamDrives=True, fileId=response['id']).execute()
189+
download_url = self.G_DRIVE_BASE_DOWNLOAD_URL.format(drive_file.get('id'))
132190
return download_url
133191

134192
def upload(self, file_name: str):
@@ -249,11 +307,11 @@ def cloneFolder(self,name,local_path,folder_id,parent_id):
249307
def create_directory(self, directory_name, parent_id):
250308
file_metadata = {
251309
"name": directory_name,
252-
"mimeType": self.__G_DRIVE_DIR_MIME_TYPE
310+
"mimeType": self.G_DRIVE_DIR_MIME_TYPE
253311
}
254312
if parent_id is not None:
255313
file_metadata["parents"] = [parent_id]
256-
file = self.__service.files().create(supportsTeamDrives=True, body=file_metadata).execute()
314+
file = service.files().create(supportsTeamDrives=True, body=file_metadata).execute()
257315
file_id = file.get("id")
258316
if not IS_TEAM_DRIVE:
259317
self.__set_permission(file_id)
@@ -280,40 +338,20 @@ def upload_dir(self, input_directory, parent_id):
280338
new_id = parent_id
281339
return new_id
282340

283-
def authorize(self):
284-
# Get credentials
285-
credentials = None
286-
if os.path.exists(self.__G_DRIVE_TOKEN_FILE):
287-
with open(self.__G_DRIVE_TOKEN_FILE, 'rb') as f:
288-
credentials = pickle.load(f)
289-
if credentials is None or not credentials.valid:
290-
if credentials and credentials.expired and credentials.refresh_token:
291-
credentials.refresh(Request())
292-
else:
293-
flow = InstalledAppFlow.from_client_secrets_file(
294-
'credentials.json', self.__OAUTH_SCOPE)
295-
LOGGER.info(flow)
296-
credentials = flow.run_console(port=0)
297-
298-
# Save the credentials for the next run
299-
with open(self.__G_DRIVE_TOKEN_FILE, 'wb') as token:
300-
pickle.dump(credentials, token)
301-
return build('drive', 'v3', credentials=credentials, cache_discovery=False)
302-
303341
def drive_list(self, fileName):
304342
msg = ""
305343
# Create Search Query for API request.
306344
query = f"'{parent_id}' in parents and (name contains '{fileName}')"
307345
page_token = None
308346
results = []
309347
while True:
310-
response = self.__service.files().list(supportsTeamDrives=True,
311-
includeTeamDriveItems=True,
312-
q=query,
313-
spaces='drive',
314-
fields='nextPageToken, files(id, name, mimeType, size)',
315-
pageToken=page_token,
316-
orderBy='modifiedTime desc').execute()
348+
response = service.files().list(supportsTeamDrives=True,
349+
includeTeamDriveItems=True,
350+
q=query,
351+
spaces='drive',
352+
fields='nextPageToken, files(id, name, mimeType, size)',
353+
pageToken=page_token,
354+
orderBy='modifiedTime desc').execute()
317355
for file in response.get('files', []):
318356
if len(results) >= 20:
319357
break

config_sample.env

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ IS_TEAM_DRIVE = ""
1212
INDEX_URL = ""
1313
USER_SESSION_STRING = ""
1414
TELEGRAM_API =
15-
TELEGRAM_HASH = ""
15+
TELEGRAM_HASH = ""
16+
USE_SERVICE_ACCOUNTS = ""

0 commit comments

Comments
 (0)