google-api-client-wrapper 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
- google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
- google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
- google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
- google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
- google_client/__init__.py +6 -0
- google_client/services/__init__.py +13 -0
- google_client/services/calendar/__init__.py +14 -0
- google_client/services/calendar/api_service.py +454 -0
- google_client/services/calendar/constants.py +48 -0
- google_client/services/calendar/exceptions.py +35 -0
- google_client/services/calendar/query_builder.py +314 -0
- google_client/services/calendar/types.py +403 -0
- google_client/services/calendar/utils.py +338 -0
- google_client/services/drive/__init__.py +13 -0
- google_client/services/drive/api_service.py +1133 -0
- google_client/services/drive/constants.py +37 -0
- google_client/services/drive/exceptions.py +60 -0
- google_client/services/drive/query_builder.py +385 -0
- google_client/services/drive/types.py +242 -0
- google_client/services/drive/utils.py +392 -0
- google_client/services/gmail/__init__.py +16 -0
- google_client/services/gmail/api_service.py +715 -0
- google_client/services/gmail/constants.py +6 -0
- google_client/services/gmail/exceptions.py +45 -0
- google_client/services/gmail/query_builder.py +408 -0
- google_client/services/gmail/types.py +285 -0
- google_client/services/gmail/utils.py +426 -0
- google_client/services/tasks/__init__.py +12 -0
- google_client/services/tasks/api_service.py +561 -0
- google_client/services/tasks/constants.py +32 -0
- google_client/services/tasks/exceptions.py +35 -0
- google_client/services/tasks/query_builder.py +324 -0
- google_client/services/tasks/types.py +156 -0
- google_client/services/tasks/utils.py +224 -0
- google_client/user_client.py +208 -0
- google_client/utils/__init__.py +0 -0
- google_client/utils/datetime.py +144 -0
- google_client/utils/validation.py +71 -0
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, List, Dict, Any, Union, BinaryIO
|
|
4
|
+
|
|
5
|
+
from googleapiclient.errors import HttpError
|
|
6
|
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseUpload, MediaIoBaseDownload
|
|
7
|
+
|
|
8
|
+
from .utils import convert_mime_type_to_downloadable, guess_extension
|
|
9
|
+
from .types import DriveFile, DriveFolder, Permission, DriveItem
|
|
10
|
+
from .query_builder import DriveQueryBuilder
|
|
11
|
+
from . import utils
|
|
12
|
+
from .constants import (
|
|
13
|
+
DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT, DEFAULT_FILE_FIELDS,
|
|
14
|
+
FOLDER_MIME_TYPE, DEFAULT_CHUNK_SIZE
|
|
15
|
+
)
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
DriveError, FileNotFoundError, FolderNotFoundError, PermissionDeniedError, FileTooLargeError,
|
|
18
|
+
UploadFailedError, DownloadFailedError, SharingError, DrivePermissionError, InvalidQueryError
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DriveApiService:
|
|
23
|
+
"""
|
|
24
|
+
Service layer for Drive API operations.
|
|
25
|
+
Contains all Drive API functionality following the user-centric approach.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, service: Any):
|
|
29
|
+
"""
|
|
30
|
+
Initialize Drive service.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
service: The Drive API service instance
|
|
34
|
+
"""
|
|
35
|
+
self._service = service
|
|
36
|
+
|
|
37
|
+
def query(self) -> DriveQueryBuilder:
|
|
38
|
+
"""
|
|
39
|
+
Create a new DriveQueryBuilder for building complex file queries with a fluent API.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
DriveQueryBuilder instance for method chaining
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
files = (user.drive.query()
|
|
46
|
+
.limit(50)
|
|
47
|
+
.in_folder("parent_folder_id")
|
|
48
|
+
.search("meeting")
|
|
49
|
+
.file_type("pdf")
|
|
50
|
+
.execute())
|
|
51
|
+
"""
|
|
52
|
+
return DriveQueryBuilder(self)
|
|
53
|
+
|
|
54
|
+
def list(
|
|
55
|
+
self,
|
|
56
|
+
query: Optional[str] = None,
|
|
57
|
+
max_results: Optional[int] = DEFAULT_MAX_RESULTS,
|
|
58
|
+
order_by: Optional[str] = None,
|
|
59
|
+
fields: Optional[str] = None,
|
|
60
|
+
page_token: Optional[str] = None
|
|
61
|
+
) -> List[DriveItem]:
|
|
62
|
+
"""
|
|
63
|
+
List files and folders in Drive.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
query: Drive API query string
|
|
67
|
+
max_results: Maximum number of items to return
|
|
68
|
+
order_by: Field to order results by
|
|
69
|
+
fields: Fields to include in response
|
|
70
|
+
page_token: Token for pagination
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of DriveFile and DriveFolder objects
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
DriveError: If the API request fails
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
if max_results and (max_results < 1 or max_results > MAX_RESULTS_LIMIT):
|
|
80
|
+
raise ValueError(f"max_results must be between 1 and {MAX_RESULTS_LIMIT}")
|
|
81
|
+
|
|
82
|
+
request_params = {
|
|
83
|
+
'pageSize': max_results or DEFAULT_MAX_RESULTS,
|
|
84
|
+
'fields': f'nextPageToken, files({fields or DEFAULT_FILE_FIELDS})'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if query:
|
|
88
|
+
request_params['q'] = query
|
|
89
|
+
if order_by:
|
|
90
|
+
request_params['orderBy'] = order_by
|
|
91
|
+
if page_token:
|
|
92
|
+
request_params['pageToken'] = page_token
|
|
93
|
+
|
|
94
|
+
result = self._service.files().list(**request_params).execute()
|
|
95
|
+
files_data = result.get('files', [])
|
|
96
|
+
|
|
97
|
+
items = [utils.convert_api_file_to_correct_type(file_data) for file_data in files_data]
|
|
98
|
+
return items
|
|
99
|
+
|
|
100
|
+
except HttpError as e:
|
|
101
|
+
error_msg = f"Failed to list files: {e}"
|
|
102
|
+
|
|
103
|
+
if e.resp.status == 403:
|
|
104
|
+
raise PermissionDeniedError(f"Permission denied: {e}")
|
|
105
|
+
elif e.resp.status == 400:
|
|
106
|
+
raise InvalidQueryError(f"Invalid query: {e}")
|
|
107
|
+
else:
|
|
108
|
+
raise DriveError(error_msg)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error_msg = f"Unexpected error listing files: {e}"
|
|
111
|
+
raise DriveError(error_msg)
|
|
112
|
+
|
|
113
|
+
def get(self, item_id: str, fields: Optional[str] = None) -> DriveItem:
|
|
114
|
+
"""
|
|
115
|
+
Get a file or folder by its id.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
item_id: File id or folder id
|
|
119
|
+
fields: Fields to include in response
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
DriveFile or DriveFolder object
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
FileNotFoundError: If the file is not found
|
|
126
|
+
DriveError: If the API request fails
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
request_params = {
|
|
130
|
+
'fileId': item_id,
|
|
131
|
+
'fields': fields or DEFAULT_FILE_FIELDS
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
result = self._service.files().get(**request_params).execute()
|
|
135
|
+
file_obj = utils.convert_api_file_to_correct_type(result)
|
|
136
|
+
return file_obj
|
|
137
|
+
|
|
138
|
+
except HttpError as e:
|
|
139
|
+
if e.resp.status == 404:
|
|
140
|
+
raise FileNotFoundError(f"File not found: {item_id}")
|
|
141
|
+
elif e.resp.status == 403:
|
|
142
|
+
raise PermissionDeniedError(f"Permission denied for file: {item_id}")
|
|
143
|
+
else:
|
|
144
|
+
error_msg = f"Failed to get file {item_id}: {e}"
|
|
145
|
+
raise DriveError(error_msg)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
error_msg = f"Unexpected error getting file {item_id}: {e}"
|
|
148
|
+
raise DriveError(error_msg)
|
|
149
|
+
|
|
150
|
+
def upload_file(
|
|
151
|
+
self,
|
|
152
|
+
file_path: str,
|
|
153
|
+
name: Optional[str] = None,
|
|
154
|
+
parent_folder_id: Optional[str] = None,
|
|
155
|
+
description: Optional[str] = None,
|
|
156
|
+
mime_type: Optional[str] = None
|
|
157
|
+
) -> DriveFile:
|
|
158
|
+
"""
|
|
159
|
+
Upload a file to Drive.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
file_path: Local path to the file to upload
|
|
163
|
+
name: Name for the file in Drive (defaults to filename)
|
|
164
|
+
parent_folder_id: ID of parent folder
|
|
165
|
+
description: File description
|
|
166
|
+
mime_type: MIME type (auto-detected if not provided)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
DriveFile object for the uploaded file
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
FileNotFoundError: If the local file doesn't exist
|
|
173
|
+
UploadFailedError: If the upload fails
|
|
174
|
+
DriveError: If the API request fails
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if not os.path.exists(file_path):
|
|
178
|
+
raise FileNotFoundError(f"Local file not found: {file_path}")
|
|
179
|
+
|
|
180
|
+
file_name = name or os.path.basename(file_path)
|
|
181
|
+
file_mime_type = mime_type or utils.guess_mime_type(file_path)
|
|
182
|
+
|
|
183
|
+
metadata = utils.build_file_metadata(
|
|
184
|
+
name=utils.sanitize_filename(file_name),
|
|
185
|
+
parents=[parent_folder_id] if parent_folder_id else None,
|
|
186
|
+
description=description
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
media = MediaFileUpload(
|
|
190
|
+
file_path,
|
|
191
|
+
mimetype=file_mime_type,
|
|
192
|
+
resumable=True,
|
|
193
|
+
chunksize=DEFAULT_CHUNK_SIZE
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
result = self._service.files().create(
|
|
197
|
+
body=metadata,
|
|
198
|
+
media_body=media,
|
|
199
|
+
fields=DEFAULT_FILE_FIELDS
|
|
200
|
+
).execute()
|
|
201
|
+
|
|
202
|
+
file_obj = utils.convert_api_file_to_drive_file(result)
|
|
203
|
+
return file_obj
|
|
204
|
+
|
|
205
|
+
except HttpError as e:
|
|
206
|
+
if e.resp.status == 403:
|
|
207
|
+
raise PermissionDeniedError(f"Permission denied uploading file: {e}")
|
|
208
|
+
elif e.resp.status == 413:
|
|
209
|
+
raise FileTooLargeError(f"File too large: {file_path}")
|
|
210
|
+
else:
|
|
211
|
+
error_msg = f"Failed to upload file {file_path}: {e}"
|
|
212
|
+
raise UploadFailedError(error_msg)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
error_msg = f"Unexpected error uploading file {file_path}: {e}"
|
|
215
|
+
raise UploadFailedError(error_msg)
|
|
216
|
+
|
|
217
|
+
def upload_file_content(
|
|
218
|
+
self,
|
|
219
|
+
content: Union[str, bytes, BinaryIO],
|
|
220
|
+
name: str,
|
|
221
|
+
parent_folder_id: Optional[str] = None,
|
|
222
|
+
description: Optional[str] = None,
|
|
223
|
+
mime_type: str = "text/plain"
|
|
224
|
+
) -> DriveFile:
|
|
225
|
+
"""
|
|
226
|
+
Upload file content directly to Drive.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
content: File content (string, bytes, or file-like object)
|
|
230
|
+
name: Name for the file in Drive
|
|
231
|
+
parent_folder_id: ID of parent folder
|
|
232
|
+
description: File description
|
|
233
|
+
mime_type: MIME type of the content
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
DriveFile object for the uploaded file
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
UploadFailedError: If the upload fails
|
|
240
|
+
DriveError: If the API request fails
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
metadata = utils.build_file_metadata(
|
|
244
|
+
name=utils.sanitize_filename(name),
|
|
245
|
+
parents=[parent_folder_id] if parent_folder_id else None,
|
|
246
|
+
description=description
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Convert content to file-like object
|
|
250
|
+
if isinstance(content, str):
|
|
251
|
+
content_io = io.StringIO(content)
|
|
252
|
+
elif isinstance(content, bytes):
|
|
253
|
+
content_io = io.BytesIO(content)
|
|
254
|
+
else:
|
|
255
|
+
content_io = content
|
|
256
|
+
|
|
257
|
+
media = MediaIoBaseUpload(
|
|
258
|
+
content_io,
|
|
259
|
+
mimetype=mime_type,
|
|
260
|
+
resumable=True
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
result = self._service.files().create(
|
|
264
|
+
body=metadata,
|
|
265
|
+
media_body=media,
|
|
266
|
+
fields=DEFAULT_FILE_FIELDS
|
|
267
|
+
).execute()
|
|
268
|
+
|
|
269
|
+
file_obj = utils.convert_api_file_to_drive_file(result)
|
|
270
|
+
return file_obj
|
|
271
|
+
|
|
272
|
+
except HttpError as e:
|
|
273
|
+
if e.resp.status == 403:
|
|
274
|
+
raise PermissionDeniedError(f"Permission denied uploading content: {e}")
|
|
275
|
+
else:
|
|
276
|
+
error_msg = f"Failed to upload content as {name}: {e}"
|
|
277
|
+
raise UploadFailedError(error_msg)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
error_msg = f"Unexpected error uploading content as {name}: {e}"
|
|
280
|
+
raise UploadFailedError(error_msg)
|
|
281
|
+
|
|
282
|
+
def download_file(self, file: DriveFile, dest_directory: str, file_name: str = None) -> str:
|
|
283
|
+
"""
|
|
284
|
+
Download a file from Drive to local disk.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
file: DriveFile object to download
|
|
288
|
+
dest_directory: Local directory where to save the file
|
|
289
|
+
file_name: Optional file name with extension
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Local path of the downloaded file
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
FileNotFoundError: If the file is not found
|
|
296
|
+
DownloadFailedError: If the download fails
|
|
297
|
+
DriveError: If the API request fails
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
# Create directory if it doesn't exist
|
|
301
|
+
os.makedirs(os.path.dirname(dest_directory), exist_ok=True)
|
|
302
|
+
file_path = ""
|
|
303
|
+
if not file_name:
|
|
304
|
+
file_name = file.name + guess_extension(file.mime_type)
|
|
305
|
+
file_path = os.path.join(dest_directory, file_name)
|
|
306
|
+
|
|
307
|
+
with open(file_path, "wb") as f:
|
|
308
|
+
f.write(self.download_file_content(file))
|
|
309
|
+
|
|
310
|
+
return dest_directory
|
|
311
|
+
|
|
312
|
+
def download_file_content(self, file: DriveFile) -> bytes:
|
|
313
|
+
"""
|
|
314
|
+
Download file content as bytes.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
file: DriveFile object to download
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
File content as bytes
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
FileNotFoundError: If the file is not found
|
|
324
|
+
DownloadFailedError: If the download fails
|
|
325
|
+
DriveError: If the API request fails
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
content_io = io.BytesIO()
|
|
329
|
+
|
|
330
|
+
request = None
|
|
331
|
+
if file.is_google_doc():
|
|
332
|
+
request = self._service.files().export_media(
|
|
333
|
+
fileId=file.file_id, mimeType=convert_mime_type_to_downloadable(file.mime_type)
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
request = self._service.files().get_media(fileId=file.file_id)
|
|
337
|
+
|
|
338
|
+
downloader = MediaIoBaseDownload(content_io, request)
|
|
339
|
+
done = False
|
|
340
|
+
while not done:
|
|
341
|
+
status, done = downloader.next_chunk()
|
|
342
|
+
|
|
343
|
+
content = content_io.getvalue()
|
|
344
|
+
return content
|
|
345
|
+
|
|
346
|
+
except HttpError as e:
|
|
347
|
+
if e.resp.status == 404:
|
|
348
|
+
raise FileNotFoundError(f"File not found: {file.file_id}")
|
|
349
|
+
elif e.resp.status == 403:
|
|
350
|
+
raise PermissionDeniedError(f"Permission denied downloading file: {file.file_id}")
|
|
351
|
+
else:
|
|
352
|
+
error_msg = f"Failed to download file content {file.file_id}: {e}"
|
|
353
|
+
raise DownloadFailedError(error_msg)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
error_msg = f"Unexpected error downloading file content {file.file_id}: {e}"
|
|
356
|
+
raise DownloadFailedError(error_msg)
|
|
357
|
+
|
|
358
|
+
def create_folder(
|
|
359
|
+
self,
|
|
360
|
+
name: str,
|
|
361
|
+
parent_folder: Optional[DriveFolder] = None,
|
|
362
|
+
description: Optional[str] = None
|
|
363
|
+
) -> DriveFolder:
|
|
364
|
+
"""
|
|
365
|
+
Create a new folder in Drive.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
name: Name of the folder
|
|
369
|
+
parent_folder: Parent DriveFolder (optional)
|
|
370
|
+
description: Folder description
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
DriveFolder object for the created folder
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
DriveError: If the API request fails
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
parent_id = parent_folder.folder_id if parent_folder else None
|
|
380
|
+
metadata = utils.build_file_metadata(
|
|
381
|
+
name=utils.sanitize_filename(name),
|
|
382
|
+
parents=[parent_id] if parent_id else None,
|
|
383
|
+
description=description,
|
|
384
|
+
mimeType=FOLDER_MIME_TYPE
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
result = self._service.files().create(
|
|
388
|
+
body=metadata,
|
|
389
|
+
fields=DEFAULT_FILE_FIELDS
|
|
390
|
+
).execute()
|
|
391
|
+
|
|
392
|
+
folder_obj = utils.convert_api_file_to_drive_folder(result)
|
|
393
|
+
return folder_obj
|
|
394
|
+
|
|
395
|
+
except HttpError as e:
|
|
396
|
+
if e.resp.status == 403:
|
|
397
|
+
raise PermissionDeniedError(f"Permission denied creating folder: {e}")
|
|
398
|
+
else:
|
|
399
|
+
error_msg = f"Failed to create folder {name}: {e}"
|
|
400
|
+
raise DriveError(error_msg)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
error_msg = f"Unexpected error creating folder {name}: {e}"
|
|
403
|
+
raise DriveError(error_msg)
|
|
404
|
+
|
|
405
|
+
def delete(self, item: DriveItem) -> bool:
|
|
406
|
+
"""
|
|
407
|
+
Delete a file or folder from Drive.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
item: DriveItem object to delete
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
True if deletion was successful
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
FileNotFoundError: If the item is not found
|
|
417
|
+
DriveError: If the API request fails
|
|
418
|
+
"""
|
|
419
|
+
try:
|
|
420
|
+
self._service.files().delete(fileId=item.item_id).execute()
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
except HttpError as e:
|
|
424
|
+
if e.resp.status == 404:
|
|
425
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
426
|
+
elif e.resp.status == 403:
|
|
427
|
+
raise PermissionDeniedError(f"Permission denied deleting item: {item.item_id}")
|
|
428
|
+
else:
|
|
429
|
+
error_msg = f"Failed to delete item {item.item_id}: {e}"
|
|
430
|
+
raise DriveError(error_msg)
|
|
431
|
+
except Exception as e:
|
|
432
|
+
error_msg = f"Unexpected error deleting item {item.item_id}: {e}"
|
|
433
|
+
raise DriveError(error_msg)
|
|
434
|
+
|
|
435
|
+
def copy(
|
|
436
|
+
self,
|
|
437
|
+
item: DriveItem,
|
|
438
|
+
new_name: Optional[str] = None,
|
|
439
|
+
parent_folder: Optional[DriveFolder] = None
|
|
440
|
+
) -> DriveItem:
|
|
441
|
+
"""
|
|
442
|
+
Copy a file or folder in Drive.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
item: DriveItem object to copy
|
|
446
|
+
new_name: Name for the copied item
|
|
447
|
+
parent_folder: Parent DriveFolder for the copy
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
DriveItem object for the copied item
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
FileNotFoundError: If the source item is not found
|
|
454
|
+
DriveError: If the API request fails
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
metadata = {}
|
|
458
|
+
if new_name:
|
|
459
|
+
metadata['name'] = utils.sanitize_filename(new_name)
|
|
460
|
+
if parent_folder:
|
|
461
|
+
metadata['parents'] = [parent_folder.folder_id]
|
|
462
|
+
|
|
463
|
+
result = self._service.files().copy(
|
|
464
|
+
fileId=item.item_id,
|
|
465
|
+
body=metadata,
|
|
466
|
+
fields=DEFAULT_FILE_FIELDS
|
|
467
|
+
).execute()
|
|
468
|
+
|
|
469
|
+
copied_item = utils.convert_api_file_to_correct_type(result)
|
|
470
|
+
return copied_item
|
|
471
|
+
|
|
472
|
+
except HttpError as e:
|
|
473
|
+
if e.resp.status == 404:
|
|
474
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
475
|
+
elif e.resp.status == 403:
|
|
476
|
+
raise PermissionDeniedError(f"Permission denied copying item: {item.item_id}")
|
|
477
|
+
else:
|
|
478
|
+
error_msg = f"Failed to copy item {item.item_id}: {e}"
|
|
479
|
+
raise DriveError(error_msg)
|
|
480
|
+
except Exception as e:
|
|
481
|
+
error_msg = f"Unexpected error copying item {item.item_id}: {e}"
|
|
482
|
+
raise DriveError(error_msg)
|
|
483
|
+
|
|
484
|
+
def rename(
|
|
485
|
+
self,
|
|
486
|
+
item: DriveItem,
|
|
487
|
+
name: Optional[str] = None,
|
|
488
|
+
) -> DriveItem:
|
|
489
|
+
"""
|
|
490
|
+
Rename a file or folder in Drive.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
item: DriveItem object to update
|
|
494
|
+
name: New name for the item
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Updated DriveItem object
|
|
498
|
+
|
|
499
|
+
Raises:
|
|
500
|
+
FileNotFoundError: If the item is not found
|
|
501
|
+
DriveError: If the API request fails
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
result = self._service.files().update(
|
|
505
|
+
fileId=item.item_id,
|
|
506
|
+
body={'name': utils.sanitize_filename(name)},
|
|
507
|
+
fields=DEFAULT_FILE_FIELDS
|
|
508
|
+
).execute()
|
|
509
|
+
|
|
510
|
+
updated_item = utils.convert_api_file_to_correct_type(result)
|
|
511
|
+
return updated_item
|
|
512
|
+
|
|
513
|
+
except HttpError as e:
|
|
514
|
+
if e.resp.status == 404:
|
|
515
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
516
|
+
elif e.resp.status == 403:
|
|
517
|
+
raise PermissionDeniedError(f"Permission denied renaming item: {item.item_id}")
|
|
518
|
+
else:
|
|
519
|
+
error_msg = f"Failed to rename item {item.item_id}: {e}"
|
|
520
|
+
raise DriveError(error_msg)
|
|
521
|
+
except Exception as e:
|
|
522
|
+
error_msg = f"Unexpected error renaming item {item.item_id}: {e}"
|
|
523
|
+
raise DriveError(error_msg)
|
|
524
|
+
|
|
525
|
+
def share(
|
|
526
|
+
self,
|
|
527
|
+
item: DriveItem,
|
|
528
|
+
email: str,
|
|
529
|
+
role: str = "reader",
|
|
530
|
+
notify: bool = True,
|
|
531
|
+
message: Optional[str] = None
|
|
532
|
+
) -> Permission:
|
|
533
|
+
"""
|
|
534
|
+
Share a file or folder with a user.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
item: DriveItem object to share
|
|
538
|
+
email: Email address of the user to share with
|
|
539
|
+
role: Permission role (reader, writer, commenter)
|
|
540
|
+
notify: Whether to send notification email
|
|
541
|
+
message: Custom message to include in notification
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Permission object for the created permission
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
FileNotFoundError: If the item is not found
|
|
548
|
+
SharingError: If sharing fails
|
|
549
|
+
DriveError: If the API request fails
|
|
550
|
+
"""
|
|
551
|
+
try:
|
|
552
|
+
permission_metadata = {
|
|
553
|
+
'type': 'user',
|
|
554
|
+
'role': role,
|
|
555
|
+
'emailAddress': email
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
result = self._service.permissions().create(
|
|
559
|
+
fileId=item.item_id,
|
|
560
|
+
body=permission_metadata,
|
|
561
|
+
sendNotificationEmail=notify,
|
|
562
|
+
emailMessage=message,
|
|
563
|
+
fields='*'
|
|
564
|
+
).execute()
|
|
565
|
+
|
|
566
|
+
permission = utils.convert_api_permission_to_permission(result)
|
|
567
|
+
return permission
|
|
568
|
+
|
|
569
|
+
except HttpError as e:
|
|
570
|
+
if e.resp.status == 404:
|
|
571
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
572
|
+
elif e.resp.status == 403:
|
|
573
|
+
raise PermissionDeniedError(f"Permission denied sharing item: {item.item_id}")
|
|
574
|
+
else:
|
|
575
|
+
error_msg = f"Failed to share item {item.item_id} with {email}: {e}"
|
|
576
|
+
raise SharingError(error_msg)
|
|
577
|
+
except Exception as e:
|
|
578
|
+
error_msg = f"Unexpected error sharing item {item.item_id} with {email}: {e}"
|
|
579
|
+
raise SharingError(error_msg)
|
|
580
|
+
|
|
581
|
+
def get_permissions(self, item: DriveItem) -> List[Permission]:
|
|
582
|
+
"""
|
|
583
|
+
Get all permissions for a file or folder.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
item: DriveItem object to get permissions for
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
List of Permission objects
|
|
590
|
+
|
|
591
|
+
Raises:
|
|
592
|
+
FileNotFoundError: If the item is not found
|
|
593
|
+
DriveError: If the API request fails
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
result = self._service.permissions().list(
|
|
597
|
+
fileId=item.item_id,
|
|
598
|
+
fields='permissions(*)'
|
|
599
|
+
).execute()
|
|
600
|
+
|
|
601
|
+
permissions_data = result.get('permissions', [])
|
|
602
|
+
permissions = [utils.convert_api_permission_to_permission(perm)
|
|
603
|
+
for perm in permissions_data]
|
|
604
|
+
return permissions
|
|
605
|
+
|
|
606
|
+
except HttpError as e:
|
|
607
|
+
if e.resp.status == 404:
|
|
608
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
609
|
+
elif e.resp.status == 403:
|
|
610
|
+
raise PermissionDeniedError(f"Permission denied getting permissions: {item.item_id}")
|
|
611
|
+
else:
|
|
612
|
+
error_msg = f"Failed to get permissions for item {item.item_id}: {e}"
|
|
613
|
+
raise DriveError(error_msg)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
error_msg = f"Unexpected error getting permissions for item {item.item_id}: {e}"
|
|
616
|
+
raise DriveError(error_msg)
|
|
617
|
+
|
|
618
|
+
def remove_permission(self, item: DriveItem, permission_id: str) -> bool:
|
|
619
|
+
"""
|
|
620
|
+
Remove a permission from a file or folder.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
item: DriveItem object to remove permission from
|
|
624
|
+
permission_id: ID of the permission to remove
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
True if removal was successful
|
|
628
|
+
|
|
629
|
+
Raises:
|
|
630
|
+
FileNotFoundError: If the item is not found
|
|
631
|
+
DrivePermissionError: If permission removal fails
|
|
632
|
+
DriveError: If the API request fails
|
|
633
|
+
"""
|
|
634
|
+
try:
|
|
635
|
+
self._service.permissions().delete(
|
|
636
|
+
fileId=item.item_id,
|
|
637
|
+
permissionId=permission_id
|
|
638
|
+
).execute()
|
|
639
|
+
return True
|
|
640
|
+
|
|
641
|
+
except HttpError as e:
|
|
642
|
+
if e.resp.status == 404:
|
|
643
|
+
raise FileNotFoundError(f"File or permission not found")
|
|
644
|
+
elif e.resp.status == 403:
|
|
645
|
+
raise PermissionDeniedError(f"Permission denied removing permission")
|
|
646
|
+
else:
|
|
647
|
+
error_msg = f"Failed to remove permission {permission_id}: {e}"
|
|
648
|
+
raise DrivePermissionError(error_msg)
|
|
649
|
+
except Exception as e:
|
|
650
|
+
error_msg = f"Unexpected error removing permission {permission_id}: {e}"
|
|
651
|
+
raise DrivePermissionError(error_msg)
|
|
652
|
+
|
|
653
|
+
def list_folder_contents(
|
|
654
|
+
self,
|
|
655
|
+
folder: DriveFolder,
|
|
656
|
+
include_folders: bool = True,
|
|
657
|
+
include_files: bool = True,
|
|
658
|
+
max_results: Optional[int] = DEFAULT_MAX_RESULTS,
|
|
659
|
+
order_by: Optional[str] = None
|
|
660
|
+
) -> List[DriveItem]:
|
|
661
|
+
"""
|
|
662
|
+
List all contents (files and/or folders) within a specific folder.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
folder: DriveFolder object representing the folder
|
|
666
|
+
include_folders: Whether to include subfolders in results
|
|
667
|
+
include_files: Whether to include files in results
|
|
668
|
+
max_results: Maximum number of items to return
|
|
669
|
+
order_by: Field to order results by
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
List of DriveFile and DriveFolder objects in the folder
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
FolderNotFoundError: If the folder is not found
|
|
676
|
+
DriveError: If the API request fails
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
query_builder = self.query().in_folder(folder.folder_id)
|
|
681
|
+
|
|
682
|
+
if include_folders and not include_files:
|
|
683
|
+
query_builder = query_builder.folders_only()
|
|
684
|
+
elif include_files and not include_folders:
|
|
685
|
+
query_builder = query_builder.files_only()
|
|
686
|
+
|
|
687
|
+
if max_results:
|
|
688
|
+
query_builder = query_builder.limit(max_results)
|
|
689
|
+
|
|
690
|
+
if order_by:
|
|
691
|
+
query_builder = query_builder.order_by(order_by)
|
|
692
|
+
|
|
693
|
+
contents = query_builder.execute()
|
|
694
|
+
|
|
695
|
+
return contents
|
|
696
|
+
|
|
697
|
+
except Exception as e:
|
|
698
|
+
if "not found" in str(e).lower():
|
|
699
|
+
raise FolderNotFoundError(f"Folder not found: {folder.folder_id}")
|
|
700
|
+
error_msg = f"Failed to list contents of folder {folder.folder_id}: {e}"
|
|
701
|
+
raise DriveError(error_msg)
|
|
702
|
+
|
|
703
|
+
def move(
|
|
704
|
+
self,
|
|
705
|
+
item: DriveItem,
|
|
706
|
+
target_folder: DriveFolder,
|
|
707
|
+
remove_from_current_parents: bool = True
|
|
708
|
+
) -> DriveItem:
|
|
709
|
+
"""
|
|
710
|
+
Move a file or folder to a different parent folder.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
item: DriveItem object to move
|
|
714
|
+
target_folder: Target DriveFolder
|
|
715
|
+
remove_from_current_parents: Whether to remove from current parents
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
Updated DriveItem object
|
|
719
|
+
|
|
720
|
+
Raises:
|
|
721
|
+
FileNotFoundError: If the item or target folder is not found
|
|
722
|
+
DriveError: If the API request fails
|
|
723
|
+
"""
|
|
724
|
+
try:
|
|
725
|
+
# Prepare the update metadata
|
|
726
|
+
update_params = {
|
|
727
|
+
'fileId': item.item_id,
|
|
728
|
+
'addParents': target_folder.folder_id,
|
|
729
|
+
'fields': DEFAULT_FILE_FIELDS
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# Remove from current parents if requested
|
|
733
|
+
if remove_from_current_parents and item.parent_ids:
|
|
734
|
+
update_params['removeParents'] = ','.join(item.parent_ids)
|
|
735
|
+
|
|
736
|
+
result = self._service.files().update(**update_params).execute()
|
|
737
|
+
|
|
738
|
+
updated_item = utils.convert_api_file_to_correct_type(result)
|
|
739
|
+
return updated_item
|
|
740
|
+
|
|
741
|
+
except HttpError as e:
|
|
742
|
+
if e.resp.status == 404:
|
|
743
|
+
raise FileNotFoundError(f"File or folder not found")
|
|
744
|
+
elif e.resp.status == 403:
|
|
745
|
+
raise PermissionDeniedError(f"Permission denied moving file")
|
|
746
|
+
else:
|
|
747
|
+
error_msg = f"Failed to move item {item.item_id}: {e}"
|
|
748
|
+
raise DriveError(error_msg)
|
|
749
|
+
except Exception as e:
|
|
750
|
+
error_msg = f"Unexpected error moving item {item.item_id}: {e}"
|
|
751
|
+
raise DriveError(error_msg)
|
|
752
|
+
|
|
753
|
+
def get_parent_folder(self, item: DriveItem) -> Optional[DriveFolder]:
|
|
754
|
+
"""
|
|
755
|
+
Get the parent folder of a file or folder.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
item: DriveItem object to get parent for
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Parent DriveFolder, or None if no parent
|
|
762
|
+
|
|
763
|
+
Raises:
|
|
764
|
+
DriveError: If the API request fails
|
|
765
|
+
"""
|
|
766
|
+
parent_id = item.get_parent_folder_id()
|
|
767
|
+
if not parent_id:
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
try:
|
|
771
|
+
result = self._service.files().get(
|
|
772
|
+
fileId=parent_id,
|
|
773
|
+
fields=DEFAULT_FILE_FIELDS
|
|
774
|
+
).execute()
|
|
775
|
+
|
|
776
|
+
parent_folder = utils.convert_api_file_to_drive_folder(result)
|
|
777
|
+
return parent_folder
|
|
778
|
+
|
|
779
|
+
except HttpError as e:
|
|
780
|
+
if e.resp.status == 404:
|
|
781
|
+
return None
|
|
782
|
+
else:
|
|
783
|
+
error_msg = f"Failed to get parent folder {parent_id}: {e}"
|
|
784
|
+
raise DriveError(error_msg)
|
|
785
|
+
except Exception as e:
|
|
786
|
+
error_msg = f"Unexpected error getting parent folder {parent_id}: {e}"
|
|
787
|
+
raise DriveError(error_msg)
|
|
788
|
+
|
|
789
|
+
def get_folder_by_path(self, path: str, root_folder_id: str = "root") -> Optional[DriveFolder]:
|
|
790
|
+
"""
|
|
791
|
+
Find a folder by its path relative to a root folder.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
path: Folder path like "/Documents/Projects" or "Documents/Projects"
|
|
795
|
+
root_folder_id: ID of the root folder to start from (default: Drive root)
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
DriveFolder object for the folder, or None if not found
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
DriveError: If the API request fails
|
|
802
|
+
"""
|
|
803
|
+
from . import utils as drive_utils
|
|
804
|
+
|
|
805
|
+
folder_names = drive_utils.parse_folder_path(path)
|
|
806
|
+
if not folder_names:
|
|
807
|
+
# Return root folder
|
|
808
|
+
try:
|
|
809
|
+
result = self._service.files().get(
|
|
810
|
+
fileId=root_folder_id,
|
|
811
|
+
fields=DEFAULT_FILE_FIELDS
|
|
812
|
+
).execute()
|
|
813
|
+
return utils.convert_api_file_to_drive_folder(result)
|
|
814
|
+
except Exception:
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
current_folder_id = root_folder_id
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
for folder_name in folder_names:
|
|
821
|
+
# Search for folder with this name in current folder
|
|
822
|
+
folders = (self.query()
|
|
823
|
+
.in_folder(current_folder_id)
|
|
824
|
+
.folders_named(folder_name)
|
|
825
|
+
.limit(1)
|
|
826
|
+
.execute())
|
|
827
|
+
|
|
828
|
+
if not folders:
|
|
829
|
+
return None
|
|
830
|
+
|
|
831
|
+
current_folder_id = folders[0].folder_id
|
|
832
|
+
|
|
833
|
+
# Get the final folder
|
|
834
|
+
result = self._service.files().get(
|
|
835
|
+
fileId=current_folder_id,
|
|
836
|
+
fields=DEFAULT_FILE_FIELDS
|
|
837
|
+
).execute()
|
|
838
|
+
|
|
839
|
+
final_folder = utils.convert_api_file_to_drive_folder(result)
|
|
840
|
+
return final_folder
|
|
841
|
+
|
|
842
|
+
except Exception as e:
|
|
843
|
+
error_msg = f"Failed to get folder by path '{path}': {e}"
|
|
844
|
+
raise DriveError(error_msg)
|
|
845
|
+
|
|
846
|
+
def create_folder_path(
|
|
847
|
+
self,
|
|
848
|
+
path: str,
|
|
849
|
+
root_folder_id: str = "root",
|
|
850
|
+
description: Optional[str] = None
|
|
851
|
+
) -> DriveFolder:
|
|
852
|
+
"""
|
|
853
|
+
Create a nested folder structure from a path, creating missing folders as needed.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
path: Folder path like "/Documents/Projects/MyProject"
|
|
857
|
+
root_folder_id: ID of the root folder to start from
|
|
858
|
+
description: Description for the final folder
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
DriveFolder object for the final folder in the path
|
|
862
|
+
|
|
863
|
+
Raises:
|
|
864
|
+
DriveError: If the API request fails
|
|
865
|
+
"""
|
|
866
|
+
from . import utils as drive_utils
|
|
867
|
+
|
|
868
|
+
folder_names = drive_utils.parse_folder_path(path)
|
|
869
|
+
if not folder_names:
|
|
870
|
+
raise ValueError("Invalid folder path")
|
|
871
|
+
|
|
872
|
+
current_folder_id = root_folder_id
|
|
873
|
+
|
|
874
|
+
try:
|
|
875
|
+
for i, folder_name in enumerate(folder_names):
|
|
876
|
+
# Check if folder already exists
|
|
877
|
+
existing_folders = (self.query()
|
|
878
|
+
.in_folder(current_folder_id)
|
|
879
|
+
.folders_named(folder_name)
|
|
880
|
+
.limit(1)
|
|
881
|
+
.execute())
|
|
882
|
+
|
|
883
|
+
if existing_folders:
|
|
884
|
+
current_folder_id = existing_folders[0].item_id
|
|
885
|
+
else:
|
|
886
|
+
# Create the folder - get parent folder object first
|
|
887
|
+
folder_desc = description if i == len(folder_names) - 1 else None
|
|
888
|
+
if current_folder_id == root_folder_id:
|
|
889
|
+
parent_folder = None # Root folder
|
|
890
|
+
else:
|
|
891
|
+
# Get parent folder as DriveFolder
|
|
892
|
+
parent_result = self._service.files().get(
|
|
893
|
+
fileId=current_folder_id,
|
|
894
|
+
fields=DEFAULT_FILE_FIELDS
|
|
895
|
+
).execute()
|
|
896
|
+
parent_folder = utils.convert_api_file_to_drive_folder(parent_result)
|
|
897
|
+
|
|
898
|
+
new_folder = self.create_folder(
|
|
899
|
+
name=folder_name,
|
|
900
|
+
parent_folder=parent_folder,
|
|
901
|
+
description=folder_desc
|
|
902
|
+
)
|
|
903
|
+
current_folder_id = new_folder.folder_id
|
|
904
|
+
|
|
905
|
+
# Return the final folder
|
|
906
|
+
result = self._service.files().get(
|
|
907
|
+
fileId=current_folder_id,
|
|
908
|
+
fields=DEFAULT_FILE_FIELDS
|
|
909
|
+
).execute()
|
|
910
|
+
|
|
911
|
+
final_folder = utils.convert_api_file_to_drive_folder(result)
|
|
912
|
+
return final_folder
|
|
913
|
+
|
|
914
|
+
except Exception as e:
|
|
915
|
+
error_msg = f"Failed to create folder path '{path}': {e}"
|
|
916
|
+
raise DriveError(error_msg)
|
|
917
|
+
|
|
918
|
+
def move_to_trash(self, item: DriveItem) -> DriveItem:
|
|
919
|
+
"""
|
|
920
|
+
Move a file or folder to trash.
|
|
921
|
+
Args:
|
|
922
|
+
item: DriveItem object to move to trash
|
|
923
|
+
Returns:
|
|
924
|
+
Updated DriveItem object
|
|
925
|
+
Raises:
|
|
926
|
+
FileNotFoundError: If the item is not found
|
|
927
|
+
DriveError: If the API request fails
|
|
928
|
+
"""
|
|
929
|
+
try:
|
|
930
|
+
result = self._service.files().update(
|
|
931
|
+
fileId=item.item_id,
|
|
932
|
+
body={'trashed': True},
|
|
933
|
+
fields=DEFAULT_FILE_FIELDS
|
|
934
|
+
).execute()
|
|
935
|
+
|
|
936
|
+
updated_item = utils.convert_api_file_to_correct_type(result)
|
|
937
|
+
return updated_item
|
|
938
|
+
|
|
939
|
+
except HttpError as e:
|
|
940
|
+
if e.resp.status == 404:
|
|
941
|
+
raise FileNotFoundError(f"Item not found: {item.item_id}")
|
|
942
|
+
else:
|
|
943
|
+
error_msg = f"Failed to move item to trash {item.item_id}: {e}"
|
|
944
|
+
raise DriveError(error_msg)
|
|
945
|
+
except Exception as e:
|
|
946
|
+
error_msg = f"Unexpected error moving item to trash {item.item_id}: {e}"
|
|
947
|
+
raise DriveError(error_msg)
|
|
948
|
+
|
|
949
|
+
def get_directory_tree(
|
|
950
|
+
self,
|
|
951
|
+
folder: DriveFolder = None,
|
|
952
|
+
max_depth: int = 3,
|
|
953
|
+
include_files: bool = True
|
|
954
|
+
) -> Dict[str, Any]:
|
|
955
|
+
"""
|
|
956
|
+
Get directory tree structure as nested dictionary.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
folder: DriveFolder to get tree structure for
|
|
960
|
+
max_depth: Maximum depth to traverse (prevents infinite loops)
|
|
961
|
+
include_files: Whether to include files in the tree
|
|
962
|
+
|
|
963
|
+
Returns:
|
|
964
|
+
Nested dictionary representing the tree structure
|
|
965
|
+
|
|
966
|
+
Raises:
|
|
967
|
+
FolderNotFoundError: If the folder is not found
|
|
968
|
+
DriveError: If the API request fails
|
|
969
|
+
"""
|
|
970
|
+
|
|
971
|
+
if not folder:
|
|
972
|
+
folder = self.get('root')
|
|
973
|
+
|
|
974
|
+
def _build_tree_recursive(current_folder: DriveFolder, current_depth: int) -> Dict[str, Any]:
|
|
975
|
+
# Build current node
|
|
976
|
+
node = {
|
|
977
|
+
'name': current_folder.name,
|
|
978
|
+
'type': 'folder',
|
|
979
|
+
'id': current_folder.folder_id,
|
|
980
|
+
'size': None,
|
|
981
|
+
'children': []
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
# Stop recursion if max depth reached
|
|
985
|
+
if current_depth >= max_depth:
|
|
986
|
+
return node
|
|
987
|
+
|
|
988
|
+
try:
|
|
989
|
+
# Get folder contents
|
|
990
|
+
contents = self.list_folder_contents(
|
|
991
|
+
current_folder,
|
|
992
|
+
include_folders=True,
|
|
993
|
+
include_files=include_files,
|
|
994
|
+
max_results=1000
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# Process each item
|
|
998
|
+
for item in contents:
|
|
999
|
+
if isinstance(item, DriveFolder):
|
|
1000
|
+
# Recursively build subtree for folders
|
|
1001
|
+
child_node = _build_tree_recursive(item, current_depth + 1)
|
|
1002
|
+
node['children'].append(child_node)
|
|
1003
|
+
elif isinstance(item, DriveFile) and include_files:
|
|
1004
|
+
# Add file node
|
|
1005
|
+
file_node = {
|
|
1006
|
+
'name': item.name,
|
|
1007
|
+
'type': 'file',
|
|
1008
|
+
'id': item.file_id,
|
|
1009
|
+
'size': item.size,
|
|
1010
|
+
'children': None
|
|
1011
|
+
}
|
|
1012
|
+
node['children'].append(file_node)
|
|
1013
|
+
|
|
1014
|
+
except (FolderNotFoundError, PermissionDeniedError) as e:
|
|
1015
|
+
# Handle permission errors gracefully
|
|
1016
|
+
node['children'] = None
|
|
1017
|
+
node['error'] = str(e)
|
|
1018
|
+
|
|
1019
|
+
return node
|
|
1020
|
+
|
|
1021
|
+
try:
|
|
1022
|
+
tree = _build_tree_recursive(folder, 0)
|
|
1023
|
+
return tree
|
|
1024
|
+
|
|
1025
|
+
except Exception as e:
|
|
1026
|
+
error_msg = f"Failed to build directory tree: {e}"
|
|
1027
|
+
raise DriveError(error_msg)
|
|
1028
|
+
|
|
1029
|
+
def print_directory_tree(
|
|
1030
|
+
self,
|
|
1031
|
+
folder: DriveFolder = None,
|
|
1032
|
+
max_depth: int = 3,
|
|
1033
|
+
show_files: bool = True,
|
|
1034
|
+
show_sizes: bool = True,
|
|
1035
|
+
show_dates: bool = False,
|
|
1036
|
+
_current_depth: int = 0,
|
|
1037
|
+
_prefix: str = ""
|
|
1038
|
+
) -> None:
|
|
1039
|
+
"""
|
|
1040
|
+
Print visual tree representation of folder structure.
|
|
1041
|
+
|
|
1042
|
+
Args:
|
|
1043
|
+
folder: DriveFolder to print tree structure for
|
|
1044
|
+
max_depth: Maximum depth to traverse
|
|
1045
|
+
show_files: Whether to include files in the output
|
|
1046
|
+
show_sizes: Whether to show file sizes
|
|
1047
|
+
show_dates: Whether to show modification dates
|
|
1048
|
+
_current_depth: Internal parameter for recursion
|
|
1049
|
+
_prefix: Internal parameter for tree formatting
|
|
1050
|
+
|
|
1051
|
+
Raises:
|
|
1052
|
+
FolderNotFoundError: If the folder is not found
|
|
1053
|
+
DriveError: If the API request fails
|
|
1054
|
+
"""
|
|
1055
|
+
|
|
1056
|
+
if not folder:
|
|
1057
|
+
folder = self.get('root')
|
|
1058
|
+
|
|
1059
|
+
# Print current folder
|
|
1060
|
+
if _current_depth == 0:
|
|
1061
|
+
print(f"📁 {folder.name}/")
|
|
1062
|
+
|
|
1063
|
+
# Stop recursion if max depth reached
|
|
1064
|
+
if _current_depth >= max_depth:
|
|
1065
|
+
return
|
|
1066
|
+
|
|
1067
|
+
try:
|
|
1068
|
+
# Get folder contents
|
|
1069
|
+
contents = self.list_folder_contents(
|
|
1070
|
+
folder,
|
|
1071
|
+
include_folders=True,
|
|
1072
|
+
include_files=show_files,
|
|
1073
|
+
max_results=1000,
|
|
1074
|
+
order_by="name"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
# Sort contents: folders first, then files
|
|
1078
|
+
folders = [item for item in contents if isinstance(item, DriveFolder)]
|
|
1079
|
+
files = [item for item in contents if isinstance(item, DriveFile)]
|
|
1080
|
+
sorted_contents = folders + files
|
|
1081
|
+
|
|
1082
|
+
for i, item in enumerate(sorted_contents):
|
|
1083
|
+
is_last = (i == len(sorted_contents) - 1)
|
|
1084
|
+
|
|
1085
|
+
# Choose tree characters
|
|
1086
|
+
if is_last:
|
|
1087
|
+
current_prefix = _prefix + "└── "
|
|
1088
|
+
next_prefix = _prefix + " "
|
|
1089
|
+
else:
|
|
1090
|
+
current_prefix = _prefix + "├── "
|
|
1091
|
+
next_prefix = _prefix + "│ "
|
|
1092
|
+
|
|
1093
|
+
# Format item display
|
|
1094
|
+
if isinstance(item, DriveFolder):
|
|
1095
|
+
# Folder display
|
|
1096
|
+
display_name = f"📁 {item.name}/"
|
|
1097
|
+
print(current_prefix + display_name)
|
|
1098
|
+
|
|
1099
|
+
# Recursively print subfolder
|
|
1100
|
+
self.print_directory_tree(
|
|
1101
|
+
item,
|
|
1102
|
+
max_depth=max_depth,
|
|
1103
|
+
show_files=show_files,
|
|
1104
|
+
show_sizes=show_sizes,
|
|
1105
|
+
show_dates=show_dates,
|
|
1106
|
+
_current_depth=_current_depth + 1,
|
|
1107
|
+
_prefix=next_prefix
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
elif isinstance(item, DriveFile):
|
|
1111
|
+
# File display
|
|
1112
|
+
display_parts = [f"📄 {item.name}"]
|
|
1113
|
+
|
|
1114
|
+
if show_sizes and item.size is not None:
|
|
1115
|
+
display_parts.append(f"({item.human_readable_size()})")
|
|
1116
|
+
|
|
1117
|
+
if show_dates and item.modified_time:
|
|
1118
|
+
from ...utils.datetime import convert_datetime_to_readable
|
|
1119
|
+
readable_date = convert_datetime_to_readable(item.modified_time)
|
|
1120
|
+
display_parts.append(f"[{readable_date}]")
|
|
1121
|
+
|
|
1122
|
+
display_name = " ".join(display_parts)
|
|
1123
|
+
print(current_prefix + display_name)
|
|
1124
|
+
|
|
1125
|
+
except (FolderNotFoundError, PermissionDeniedError) as e:
|
|
1126
|
+
# Handle permission errors gracefully
|
|
1127
|
+
error_prefix = _prefix + "└── " if _current_depth > 0 else ""
|
|
1128
|
+
print(f"{error_prefix}❌ Access denied: {e}")
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
error_msg = f"Error displaying folder contents: {e}"
|
|
1131
|
+
if _current_depth == 0:
|
|
1132
|
+
raise DriveError(error_msg)
|
|
1133
|
+
|