2025-05-12 13:52:53 -04:00
"""
Google Drive MCP Tools
This module provides MCP tools for interacting with Google Drive API.
"""
2025-12-13 13:49:28 -08:00
2025-05-12 13:52:53 -04:00
import logging
import asyncio
2025-08-19 14:15:04 -04:00
from typing import Optional
2025-10-16 21:13:42 -04:00
from tempfile import NamedTemporaryFile
2025-11-10 11:03:38 -05:00
from urllib.parse import urlparse
from urllib.request import url2pathname
2025-11-09 17:12:26 +00:00
from pathlib import Path
2025-05-12 13:52:53 -04:00
2025-06-03 14:16:19 -04:00
from googleapiclient.http import MediaIoBaseDownload , MediaIoBaseUpload
2025-05-30 11:09:56 -04:00
import io
2025-06-18 11:12:01 -04:00
import httpx
2025-11-29 16:09:07 +01:00
import base64
2025-05-12 13:52:53 -04:00
2025-06-07 10:33:30 -04:00
from auth.service_decorator import require_google_service
2025-10-19 11:59:56 -04:00
from auth.oauth_config import is_stateless_mode
2025-07-17 13:57:21 -04:00
from core.utils import extract_office_xml_text , handle_http_errors
2025-05-24 10:43:55 -04:00
from core.server import server
2025-11-10 11:13:47 -05:00
from core.config import get_transport_mode
2025-11-11 16:43:00 +08:00
from gdrive.drive_helpers import (
DRIVE_QUERY_PATTERNS ,
build_drive_list_params ,
resolve_drive_item ,
resolve_folder_id ,
)
2025-05-12 13:52:53 -04:00
logger = logging . getLogger ( __name__ )
2025-10-19 14:14:20 -04:00
DOWNLOAD_CHUNK_SIZE_BYTES = 256 * 1024 # 256 KB
UPLOAD_CHUNK_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB (Google recommended minimum)
2025-12-13 13:49:28 -08:00
2025-05-12 13:52:53 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "search_drive_files" , is_read_only = True , service_type = "drive" )
2025-06-07 10:33:30 -04:00
@require_google_service ( "drive" , "drive_read" )
2025-05-12 13:52:53 -04:00
async def search_drive_files (
2025-06-07 10:33:30 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-12 13:52:53 -04:00
query : str ,
page_size : int = 10 ,
2025-06-04 18:13:45 -04:00
drive_id : Optional [ str ] = None ,
include_items_from_all_drives : bool = True ,
corpora : Optional [ str ] = None ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 13:52:53 -04:00
"""
2025-06-04 18:13:45 -04:00
Searches for files and folders within a user's Google Drive, including shared drives.
2025-05-12 14:32:44 -04:00
Args:
2025-05-24 10:43:55 -04:00
user_google_email (str): The user's Google email address. Required.
2025-06-04 18:13:45 -04:00
query (str): The search query string. Supports Google Drive search operators.
2025-05-12 14:32:44 -04:00
page_size (int): The maximum number of files to return. Defaults to 10.
2025-06-04 18:13:45 -04:00
drive_id (Optional[str]): ID of the shared drive to search. If None, behavior depends on `corpora` and `include_items_from_all_drives`.
include_items_from_all_drives (bool): Whether shared drive items should be included in results. Defaults to True. This is effective when not specifying a `drive_id`.
corpora (Optional[str]): Bodies of items to query (e.g., 'user', 'domain', 'drive', 'allDrives').
If 'drive_id' is specified and 'corpora' is None, it defaults to 'drive'.
Otherwise, Drive API default behavior applies. Prefer 'user' or 'drive' over 'allDrives' for efficiency.
2025-05-12 14:32:44 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: A formatted list of found files/folders with their details (ID, name, type, size, modified time, link).
2025-05-12 13:52:53 -04:00
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[search_drive_files] Invoked. Email: ' { user_google_email } ', Query: ' { query } '"
)
2025-05-13 12:36:53 -04:00
2025-06-18 16:29:35 -04:00
# Check if the query looks like a structured Drive query or free text
# Look for Drive API operators and structured query patterns
is_structured_query = any ( pattern . search ( query ) for pattern in DRIVE_QUERY_PATTERNS )
if is_structured_query :
final_query = query
2025-12-13 13:49:28 -08:00
logger . info (
f "[search_drive_files] Using structured query as-is: ' { final_query } '"
)
2025-06-18 16:29:35 -04:00
else :
# For free text queries, wrap in fullText contains
escaped_query = query . replace ( "'" , " \\ '" )
final_query = f "fullText contains ' { escaped_query } '"
2025-12-13 13:49:28 -08:00
logger . info (
f "[search_drive_files] Reformatting free text query ' { query } ' to ' { final_query } '"
)
2025-06-18 16:29:35 -04:00
2025-08-19 14:15:04 -04:00
list_params = build_drive_list_params (
2025-06-18 16:29:35 -04:00
query = final_query ,
page_size = page_size ,
drive_id = drive_id ,
include_items_from_all_drives = include_items_from_all_drives ,
corpora = corpora ,
)
2025-12-13 13:49:28 -08:00
results = await asyncio . to_thread ( service . files () . list ( ** list_params ) . execute )
files = results . get ( "files" , [])
2025-06-18 16:29:35 -04:00
if not files :
return f "No files found for ' { query } '."
2025-12-13 13:49:28 -08:00
formatted_files_text_parts = [
f "Found { len ( files ) } files for { user_google_email } matching ' { query } ':"
]
2025-06-18 16:29:35 -04:00
for item in files :
2025-12-13 13:49:28 -08:00
size_str = f ", Size: { item . get ( 'size' , 'N/A' ) } " if "size" in item else ""
2025-06-18 16:29:35 -04:00
formatted_files_text_parts . append (
2025-12-13 13:49:28 -08:00
f '- Name: " { item [ "name" ] } " (ID: { item [ "id" ] } , Type: { item [ "mimeType" ] }{ size_str } , Modified: { item . get ( "modifiedTime" , "N/A" ) } ) Link: { item . get ( "webViewLink" , "#" ) } '
2025-06-04 18:50:53 -04:00
)
2025-06-18 16:29:35 -04:00
text_output = " \n " . join ( formatted_files_text_parts )
return text_output
2025-05-12 13:52:53 -04:00
2025-12-13 13:49:28 -08:00
2025-05-12 13:52:53 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "get_drive_file_content" , is_read_only = True , service_type = "drive" )
2025-06-07 10:33:30 -04:00
@require_google_service ( "drive" , "drive_read" )
2025-05-12 13:52:53 -04:00
async def get_drive_file_content (
2025-06-07 10:33:30 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-12 13:52:53 -04:00
file_id : str ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 13:52:53 -04:00
"""
2025-06-04 18:13:45 -04:00
Retrieves the content of a specific Google Drive file by ID, supporting files in shared drives.
2025-05-24 13:49:04 -04:00
• Native Google Docs, Sheets, Slides → exported as text / CSV.
• Office files (.docx, .xlsx, .pptx) → unzipped & parsed with std-lib to
extract readable text.
• Any other file → downloaded; tries UTF-8 decode, else notes binary.
2025-05-12 14:32:44 -04:00
Args:
2025-05-24 13:49:04 -04:00
user_google_email: The user’ s Google email address.
file_id: Drive file ID.
2025-05-12 14:32:44 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: The file content as plain text with metadata header.
2025-05-12 13:52:53 -04:00
"""
2025-06-07 10:33:30 -04:00
logger . info ( f "[get_drive_file_content] Invoked. File ID: ' { file_id } '" )
2025-05-12 13:52:53 -04:00
2025-11-11 16:43:00 +08:00
resolved_file_id , file_metadata = await resolve_drive_item (
service ,
file_id ,
extra_fields = "name, webViewLink" ,
2025-06-18 16:29:35 -04:00
)
2025-11-11 16:43:00 +08:00
file_id = resolved_file_id
2025-06-18 16:29:35 -04:00
mime_type = file_metadata . get ( "mimeType" , "" )
file_name = file_metadata . get ( "name" , "Unknown File" )
export_mime_type = {
"application/vnd.google-apps.document" : "text/plain" ,
"application/vnd.google-apps.spreadsheet" : "text/csv" ,
"application/vnd.google-apps.presentation" : "text/plain" ,
} . get ( mime_type )
request_obj = (
2025-11-11 16:43:00 +08:00
service . files () . export_media ( fileId = file_id , mimeType = export_mime_type )
2025-06-18 16:29:35 -04:00
if export_mime_type
2025-11-11 16:43:00 +08:00
else service . files () . get_media ( fileId = file_id )
2025-06-18 16:29:35 -04:00
)
fh = io . BytesIO ()
downloader = MediaIoBaseDownload ( fh , request_obj )
loop = asyncio . get_event_loop ()
done = False
while not done :
status , done = await loop . run_in_executor ( None , downloader . next_chunk )
file_content_bytes = fh . getvalue ()
# Attempt Office XML extraction only for actual Office XML files
office_mime_types = {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ,
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ,
2025-12-13 13:49:28 -08:00
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ,
2025-06-18 16:29:35 -04:00
}
2025-07-17 13:57:21 -04:00
2025-06-18 16:29:35 -04:00
if mime_type in office_mime_types :
office_text = extract_office_xml_text ( file_content_bytes , mime_type )
if office_text :
body_text = office_text
2025-05-24 13:49:04 -04:00
else :
2025-06-18 16:29:35 -04:00
# Fallback: try UTF-8; otherwise flag binary
2025-05-24 13:49:04 -04:00
try :
body_text = file_content_bytes . decode ( "utf-8" )
except UnicodeDecodeError :
body_text = (
2025-05-24 14:49:32 -04:00
f "[Binary or unsupported text encoding for mimeType ' { mime_type } ' - "
2025-05-24 13:49:04 -04:00
f " { len ( file_content_bytes ) } bytes]"
)
2025-06-18 16:29:35 -04:00
else :
# For non-Office files (including Google native files), try UTF-8 decode directly
try :
body_text = file_content_bytes . decode ( "utf-8" )
except UnicodeDecodeError :
body_text = (
f "[Binary or unsupported text encoding for mimeType ' { mime_type } ' - "
f " { len ( file_content_bytes ) } bytes]"
)
2025-05-24 13:49:04 -04:00
2025-06-18 16:29:35 -04:00
# Assemble response
header = (
f 'File: " { file_name } " (ID: { file_id } , Type: { mime_type } ) \n '
2025-12-13 13:49:28 -08:00
f "Link: { file_metadata . get ( 'webViewLink' , '#' ) } \n\n --- CONTENT --- \n "
2025-06-18 16:29:35 -04:00
)
return header + body_text
2025-05-24 13:49:04 -04:00
2025-05-12 13:52:53 -04:00
2025-11-29 16:09:07 +01:00
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_drive_file_download_url" , is_read_only = True , service_type = "drive"
)
2025-11-29 16:09:07 +01:00
@require_google_service ( "drive" , "drive_read" )
async def get_drive_file_download_url (
service ,
user_google_email : str ,
file_id : str ,
export_format : Optional [ str ] = None ,
) -> str :
"""
Gets a download URL for a Google Drive file. The file is prepared and made available via HTTP URL.
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
For Google native files (Docs, Sheets, Slides), exports to a useful format:
• Google Docs → PDF (default) or DOCX if export_format='docx'
• Google Sheets → XLSX (default) or CSV if export_format='csv'
• Google Slides → PDF (default) or PPTX if export_format='pptx'
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
For other files, downloads the original file format.
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
Args:
user_google_email: The user's Google email address. Required.
file_id: The Google Drive file ID to get a download URL for.
2025-12-13 13:49:28 -08:00
export_format: Optional export format for Google native files.
Options: 'pdf', 'docx', 'xlsx', 'csv', 'pptx'.
2025-11-29 16:09:07 +01:00
If not specified, uses sensible defaults (PDF for Docs/Slides, XLSX for Sheets).
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
Returns:
str: Download URL and file metadata. The file is available at the URL for 1 hour.
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[get_drive_file_download_url] Invoked. File ID: ' { file_id } ', Export format: { export_format } "
)
2025-11-29 16:09:07 +01:00
# Resolve shortcuts and get file metadata
resolved_file_id , file_metadata = await resolve_drive_item (
service ,
file_id ,
extra_fields = "name, webViewLink, mimeType" ,
)
file_id = resolved_file_id
mime_type = file_metadata . get ( "mimeType" , "" )
file_name = file_metadata . get ( "name" , "Unknown File" )
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Determine export format for Google native files
export_mime_type = None
output_filename = file_name
output_mime_type = mime_type
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
if mime_type == "application/vnd.google-apps.document" :
# Google Docs
if export_format == "docx" :
export_mime_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
output_mime_type = export_mime_type
if not output_filename . endswith ( ".docx" ):
output_filename = f " { Path ( output_filename ) . stem } .docx"
else :
# Default to PDF
export_mime_type = "application/pdf"
output_mime_type = export_mime_type
if not output_filename . endswith ( ".pdf" ):
output_filename = f " { Path ( output_filename ) . stem } .pdf"
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
elif mime_type == "application/vnd.google-apps.spreadsheet" :
# Google Sheets
if export_format == "csv" :
export_mime_type = "text/csv"
output_mime_type = export_mime_type
if not output_filename . endswith ( ".csv" ):
output_filename = f " { Path ( output_filename ) . stem } .csv"
else :
# Default to XLSX
2025-12-13 13:49:28 -08:00
export_mime_type = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
2025-11-29 16:09:07 +01:00
output_mime_type = export_mime_type
if not output_filename . endswith ( ".xlsx" ):
output_filename = f " { Path ( output_filename ) . stem } .xlsx"
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
elif mime_type == "application/vnd.google-apps.presentation" :
# Google Slides
if export_format == "pptx" :
export_mime_type = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
output_mime_type = export_mime_type
if not output_filename . endswith ( ".pptx" ):
output_filename = f " { Path ( output_filename ) . stem } .pptx"
else :
# Default to PDF
export_mime_type = "application/pdf"
output_mime_type = export_mime_type
if not output_filename . endswith ( ".pdf" ):
output_filename = f " { Path ( output_filename ) . stem } .pdf"
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Download the file
request_obj = (
service . files () . export_media ( fileId = file_id , mimeType = export_mime_type )
if export_mime_type
else service . files () . get_media ( fileId = file_id )
)
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
fh = io . BytesIO ()
downloader = MediaIoBaseDownload ( fh , request_obj )
loop = asyncio . get_event_loop ()
done = False
while not done :
status , done = await loop . run_in_executor ( None , downloader . next_chunk )
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
file_content_bytes = fh . getvalue ()
size_bytes = len ( file_content_bytes )
size_kb = size_bytes / 1024 if size_bytes else 0
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Check if we're in stateless mode (can't save files)
if is_stateless_mode ():
result_lines = [
"File downloaded successfully!" ,
f "File: { file_name } " ,
f "File ID: { file_id } " ,
f "Size: { size_kb : .1f } KB ( { size_bytes } bytes)" ,
f "MIME Type: { output_mime_type } " ,
" \n ⚠️ Stateless mode: File storage disabled." ,
" \n Base64-encoded content (first 100 characters shown):" ,
f " { base64 . b64encode ( file_content_bytes [: 100 ]) . decode ( 'utf-8' ) } ..." ,
]
2025-12-13 13:49:28 -08:00
logger . info (
f "[get_drive_file_download_url] Successfully downloaded { size_kb : .1f } KB file (stateless mode)"
)
2025-11-29 16:09:07 +01:00
return " \n " . join ( result_lines )
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Save file and generate URL
try :
from core.attachment_storage import get_attachment_storage , get_attachment_url
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
storage = get_attachment_storage ()
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Encode bytes to base64 (as expected by AttachmentStorage)
2025-12-13 13:49:28 -08:00
base64_data = base64 . urlsafe_b64encode ( file_content_bytes ) . decode ( "utf-8" )
2025-11-29 16:09:07 +01:00
# Save attachment
saved_file_id = storage . save_attachment (
base64_data = base64_data ,
filename = output_filename ,
2025-12-13 13:49:28 -08:00
mime_type = output_mime_type ,
2025-11-29 16:09:07 +01:00
)
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
# Generate URL
download_url = get_attachment_url ( saved_file_id )
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
result_lines = [
"File downloaded successfully!" ,
f "File: { file_name } " ,
f "File ID: { file_id } " ,
f "Size: { size_kb : .1f } KB ( { size_bytes } bytes)" ,
f "MIME Type: { output_mime_type } " ,
f " \n 📎 Download URL: { download_url } " ,
" \n The file has been saved and is available at the URL above." ,
"The file will expire after 1 hour." ,
]
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
if export_mime_type :
2025-12-13 13:49:28 -08:00
result_lines . append (
f " \n Note: Google native file exported to { output_mime_type } format."
)
logger . info (
f "[get_drive_file_download_url] Successfully saved { size_kb : .1f } KB file as { saved_file_id } "
)
2025-11-29 16:09:07 +01:00
return " \n " . join ( result_lines )
2025-12-13 13:49:28 -08:00
2025-11-29 16:09:07 +01:00
except Exception as e :
logger . error ( f "[get_drive_file_download_url] Failed to save file: { e } " )
return (
f "Error: Failed to save file for download. \n "
f "File was downloaded successfully ( { size_kb : .1f } KB) but could not be saved. \n\n "
f "Error details: { str ( e ) } "
)
2025-06-18 16:29:35 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "list_drive_items" , is_read_only = True , service_type = "drive" )
2025-06-07 10:33:30 -04:00
@require_google_service ( "drive" , "drive_read" )
2025-05-12 13:52:53 -04:00
async def list_drive_items (
2025-06-07 10:33:30 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-12-13 13:49:28 -08:00
folder_id : str = "root" ,
2025-05-30 11:09:56 -04:00
page_size : int = 100 ,
2025-06-04 18:13:45 -04:00
drive_id : Optional [ str ] = None ,
include_items_from_all_drives : bool = True ,
corpora : Optional [ str ] = None ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 13:52:53 -04:00
"""
2025-06-04 18:13:45 -04:00
Lists files and folders, supporting shared drives.
If `drive_id` is specified, lists items within that shared drive. `folder_id` is then relative to that drive (or use drive_id as folder_id for root).
If `drive_id` is not specified, lists items from user's "My Drive" and accessible shared drives (if `include_items_from_all_drives` is True).
2025-05-12 14:32:44 -04:00
Args:
2025-05-24 10:43:55 -04:00
user_google_email (str): The user's Google email address. Required.
2025-06-04 18:13:45 -04:00
folder_id (str): The ID of the Google Drive folder. Defaults to 'root'. For a shared drive, this can be the shared drive's ID to list its root, or a folder ID within that shared drive.
page_size (int): The maximum number of items to return. Defaults to 100.
drive_id (Optional[str]): ID of the shared drive. If provided, the listing is scoped to this drive.
include_items_from_all_drives (bool): Whether items from all accessible shared drives should be included if `drive_id` is not set. Defaults to True.
corpora (Optional[str]): Corpus to query ('user', 'drive', 'allDrives'). If `drive_id` is set and `corpora` is None, 'drive' is used. If None and no `drive_id`, API defaults apply.
2025-05-12 14:32:44 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: A formatted list of files/folders in the specified folder.
2025-05-12 13:52:53 -04:00
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[list_drive_items] Invoked. Email: ' { user_google_email } ', Folder ID: ' { folder_id } '"
)
2025-05-12 13:52:53 -04:00
2025-11-11 16:43:00 +08:00
resolved_folder_id = await resolve_folder_id ( service , folder_id )
final_query = f "' { resolved_folder_id } ' in parents and trashed=false"
2025-06-18 16:29:35 -04:00
2025-08-19 14:15:04 -04:00
list_params = build_drive_list_params (
2025-06-18 16:29:35 -04:00
query = final_query ,
page_size = page_size ,
drive_id = drive_id ,
include_items_from_all_drives = include_items_from_all_drives ,
corpora = corpora ,
)
2025-12-13 13:49:28 -08:00
results = await asyncio . to_thread ( service . files () . list ( ** list_params ) . execute )
files = results . get ( "files" , [])
2025-06-18 16:29:35 -04:00
if not files :
return f "No items found in folder ' { folder_id } '."
2025-12-13 13:49:28 -08:00
formatted_items_text_parts = [
f "Found { len ( files ) } items in folder ' { folder_id } ' for { user_google_email } :"
]
2025-06-18 16:29:35 -04:00
for item in files :
2025-12-13 13:49:28 -08:00
size_str = f ", Size: { item . get ( 'size' , 'N/A' ) } " if "size" in item else ""
2025-06-18 16:29:35 -04:00
formatted_items_text_parts . append (
2025-12-13 13:49:28 -08:00
f '- Name: " { item [ "name" ] } " (ID: { item [ "id" ] } , Type: { item [ "mimeType" ] }{ size_str } , Modified: { item . get ( "modifiedTime" , "N/A" ) } ) Link: { item . get ( "webViewLink" , "#" ) } '
2025-05-12 13:52:53 -04:00
)
2025-06-18 16:29:35 -04:00
text_output = " \n " . join ( formatted_items_text_parts )
return text_output
2025-05-12 13:52:53 -04:00
2025-12-13 13:49:28 -08:00
2025-06-18 08:15:49 -03:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "create_drive_file" , service_type = "drive" )
2025-07-17 13:57:21 -04:00
@require_google_service ( "drive" , "drive_file" )
2025-05-12 13:52:53 -04:00
async def create_drive_file (
2025-06-07 10:33:30 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-12 13:52:53 -04:00
file_name : str ,
2025-06-18 08:15:49 -03:00
content : Optional [ str ] = None , # Now explicitly Optional
2025-12-13 13:49:28 -08:00
folder_id : str = "root" ,
mime_type : str = "text/plain" ,
2025-06-18 08:15:49 -03:00
fileUrl : Optional [ str ] = None , # Now explicitly Optional
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 13:52:53 -04:00
"""
2025-06-04 18:47:16 -04:00
Creates a new file in Google Drive, supporting creation within shared drives.
2025-06-18 08:15:49 -03:00
Accepts either direct content or a fileUrl to fetch the content from.
2025-05-12 14:32:44 -04:00
Args:
2025-05-24 10:43:55 -04:00
user_google_email (str): The user's Google email address. Required.
2025-05-13 12:36:53 -04:00
file_name (str): The name for the new file.
2025-06-18 08:15:49 -03:00
content (Optional[str]): If provided, the content to write to the file.
2025-06-04 18:13:45 -04:00
folder_id (str): The ID of the parent folder. Defaults to 'root'. For shared drives, this must be a folder ID within the shared drive.
2025-05-13 12:36:53 -04:00
mime_type (str): The MIME type of the file. Defaults to 'text/plain'.
2025-11-09 17:12:26 +00:00
fileUrl (Optional[str]): If provided, fetches the file content from this URL. Supports file://, http://, and https:// protocols.
2025-05-12 14:32:44 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message of the successful file creation with file link.
2025-05-12 13:52:53 -04:00
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[create_drive_file] Invoked. Email: ' { user_google_email } ', File Name: { file_name } , Folder ID: { folder_id } , fileUrl: { fileUrl } "
)
2025-05-12 13:52:53 -04:00
2025-06-18 17:08:00 -04:00
if not content and not fileUrl :
raise Exception ( "You must provide either 'content' or 'fileUrl'." )
file_data = None
2025-11-11 16:43:00 +08:00
resolved_folder_id = await resolve_folder_id ( service , folder_id )
2025-05-12 13:52:53 -04:00
2025-06-18 16:29:35 -04:00
file_metadata = {
2025-12-13 13:49:28 -08:00
"name" : file_name ,
"parents" : [ resolved_folder_id ],
"mimeType" : mime_type ,
2025-06-18 16:29:35 -04:00
}
2025-10-16 21:13:42 -04:00
# Prefer fileUrl if both are provided
if fileUrl :
logger . info ( f "[create_drive_file] Fetching file from URL: { fileUrl } " )
2025-11-09 17:12:26 +00:00
# Check if this is a file:// URL
parsed_url = urlparse ( fileUrl )
2025-12-13 13:49:28 -08:00
if parsed_url . scheme == "file" :
2025-11-09 17:12:26 +00:00
# Handle file:// URL - read from local filesystem
2025-12-13 13:49:28 -08:00
logger . info (
"[create_drive_file] Detected file:// URL, reading from local filesystem"
)
2025-11-10 11:13:47 -05:00
transport_mode = get_transport_mode ()
running_streamable = transport_mode == "streamable-http"
if running_streamable :
2025-12-13 13:49:28 -08:00
logger . warning (
"[create_drive_file] file:// URL requested while server runs in streamable-http mode. Ensure the file path is accessible to the server (e.g., Docker volume) or use an HTTP(S) URL."
)
2025-11-09 17:12:26 +00:00
2025-11-10 11:03:38 -05:00
# Convert file:// URL to a cross-platform local path
raw_path = parsed_url . path or ""
netloc = parsed_url . netloc
if netloc and netloc . lower () != "localhost" :
raw_path = f "// { netloc }{ raw_path } "
file_path = url2pathname ( raw_path )
2025-11-09 17:12:26 +00:00
# Verify file exists
path_obj = Path ( file_path )
if not path_obj . exists ():
2025-12-13 13:49:28 -08:00
extra = (
" The server is running via streamable-http, so file:// URLs must point to files inside the container or remote host."
if running_streamable
else ""
)
2025-11-10 11:13:47 -05:00
raise Exception ( f "Local file does not exist: { file_path } . { extra } " )
2025-11-09 17:12:26 +00:00
if not path_obj . is_file ():
2025-12-13 13:49:28 -08:00
extra = (
" In streamable-http/Docker deployments, mount the file into the container or provide an HTTP(S) URL."
if running_streamable
else ""
)
2025-11-10 11:13:47 -05:00
raise Exception ( f "Path is not a file: { file_path } . { extra } " )
2025-11-09 17:12:26 +00:00
logger . info ( f "[create_drive_file] Reading local file: { file_path } " )
# Read file and upload
file_data = await asyncio . to_thread ( path_obj . read_bytes )
total_bytes = len ( file_data )
logger . info ( f "[create_drive_file] Read { total_bytes } bytes from local file" )
2025-10-16 21:13:42 -04:00
2025-10-19 14:18:06 -04:00
media = MediaIoBaseUpload (
io . BytesIO ( file_data ),
mimetype = mime_type ,
resumable = True ,
2025-12-13 13:49:28 -08:00
chunksize = UPLOAD_CHUNK_SIZE_BYTES ,
2025-10-19 14:18:06 -04:00
)
2025-11-09 17:12:26 +00:00
logger . info ( "[create_drive_file] Starting upload to Google Drive..." )
2025-10-16 21:13:42 -04:00
created_file = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. create (
2025-10-16 21:13:42 -04:00
body = file_metadata ,
2025-10-19 14:18:06 -04:00
media_body = media ,
2025-12-13 13:49:28 -08:00
fields = "id, name, webViewLink" ,
supportsAllDrives = True ,
)
. execute
2025-10-16 21:13:42 -04:00
)
2025-11-09 17:12:26 +00:00
# Handle HTTP/HTTPS URLs
2025-12-13 13:49:28 -08:00
elif parsed_url . scheme in ( "http" , "https" ):
2025-11-09 17:12:26 +00:00
# when running in stateless mode, deployment may not have access to local file system
if is_stateless_mode ():
2025-10-19 11:59:56 -04:00
async with httpx . AsyncClient ( follow_redirects = True ) as client :
2025-11-09 17:12:26 +00:00
resp = await client . get ( fileUrl )
if resp . status_code != 200 :
2025-12-13 13:49:28 -08:00
raise Exception (
f "Failed to fetch file from URL: { fileUrl } (status { resp . status_code } )"
)
2025-11-09 17:12:26 +00:00
file_data = await resp . aread ()
# Try to get MIME type from Content-Type header
content_type = resp . headers . get ( "Content-Type" )
if content_type and content_type != "application/octet-stream" :
mime_type = content_type
2025-12-13 13:49:28 -08:00
file_metadata [ "mimeType" ] = content_type
logger . info (
f "[create_drive_file] Using MIME type from Content-Type header: { content_type } "
)
2025-10-19 11:59:56 -04:00
media = MediaIoBaseUpload (
2025-11-09 17:12:26 +00:00
io . BytesIO ( file_data ),
2025-10-19 11:59:56 -04:00
mimetype = mime_type ,
resumable = True ,
2025-12-13 13:49:28 -08:00
chunksize = UPLOAD_CHUNK_SIZE_BYTES ,
2025-10-19 11:59:56 -04:00
)
created_file = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. create (
2025-10-19 11:59:56 -04:00
body = file_metadata ,
media_body = media ,
2025-12-13 13:49:28 -08:00
fields = "id, name, webViewLink" ,
supportsAllDrives = True ,
)
. execute
2025-10-19 11:59:56 -04:00
)
2025-11-09 17:12:26 +00:00
else :
# Use NamedTemporaryFile to stream download and upload
with NamedTemporaryFile () as temp_file :
total_bytes = 0
# follow redirects
async with httpx . AsyncClient ( follow_redirects = True ) as client :
2025-12-13 13:49:28 -08:00
async with client . stream ( "GET" , fileUrl ) as resp :
2025-11-09 17:12:26 +00:00
if resp . status_code != 200 :
2025-12-13 13:49:28 -08:00
raise Exception (
f "Failed to fetch file from URL: { fileUrl } (status { resp . status_code } )"
)
2025-11-09 17:12:26 +00:00
# Stream download in chunks
2025-12-13 13:49:28 -08:00
async for chunk in resp . aiter_bytes (
chunk_size = DOWNLOAD_CHUNK_SIZE_BYTES
):
2025-11-09 17:12:26 +00:00
await asyncio . to_thread ( temp_file . write , chunk )
total_bytes += len ( chunk )
2025-12-13 13:49:28 -08:00
logger . info (
f "[create_drive_file] Downloaded { total_bytes } bytes from URL before upload."
)
2025-11-09 17:12:26 +00:00
# Try to get MIME type from Content-Type header
content_type = resp . headers . get ( "Content-Type" )
2025-12-13 13:49:28 -08:00
if (
content_type
and content_type != "application/octet-stream"
):
2025-11-09 17:12:26 +00:00
mime_type = content_type
2025-12-13 13:49:28 -08:00
file_metadata [ "mimeType" ] = mime_type
logger . info (
f "[create_drive_file] Using MIME type from Content-Type header: { mime_type } "
)
2025-11-09 17:12:26 +00:00
# Reset file pointer to beginning for upload
temp_file . seek ( 0 )
# Upload with chunking
media = MediaIoBaseUpload (
temp_file ,
mimetype = mime_type ,
resumable = True ,
2025-12-13 13:49:28 -08:00
chunksize = UPLOAD_CHUNK_SIZE_BYTES ,
2025-11-09 17:12:26 +00:00
)
2025-12-13 13:49:28 -08:00
logger . info (
"[create_drive_file] Starting upload to Google Drive..."
)
2025-11-09 17:12:26 +00:00
created_file = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. create (
2025-11-09 17:12:26 +00:00
body = file_metadata ,
media_body = media ,
2025-12-13 13:49:28 -08:00
fields = "id, name, webViewLink" ,
supportsAllDrives = True ,
)
. execute
2025-11-09 17:12:26 +00:00
)
else :
2025-11-10 11:03:38 -05:00
if not parsed_url . scheme :
2025-12-13 13:49:28 -08:00
raise Exception (
"fileUrl is missing a URL scheme. Use file://, http://, or https://."
)
raise Exception (
f "Unsupported URL scheme ' { parsed_url . scheme } '. Only file://, http://, and https:// are supported."
)
2025-10-16 21:13:42 -04:00
elif content :
2025-12-13 13:49:28 -08:00
file_data = content . encode ( "utf-8" )
2025-10-16 21:13:42 -04:00
media = io . BytesIO ( file_data )
created_file = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. create (
2025-10-16 21:13:42 -04:00
body = file_metadata ,
media_body = MediaIoBaseUpload ( media , mimetype = mime_type , resumable = True ),
2025-12-13 13:49:28 -08:00
fields = "id, name, webViewLink" ,
supportsAllDrives = True ,
)
. execute
2025-10-16 21:13:42 -04:00
)
2025-06-18 16:29:35 -04:00
2025-12-13 13:49:28 -08:00
link = created_file . get ( "webViewLink" , "No link available" )
2025-06-18 16:29:35 -04:00
confirmation_message = f "Successfully created file ' { created_file . get ( 'name' , file_name ) } ' (ID: { created_file . get ( 'id' , 'N/A' ) } ) in folder ' { folder_id } ' for { user_google_email } . Link: { link } "
logger . info ( f "Successfully created file. Link: { link } " )
2025-08-19 14:15:04 -04:00
return confirmation_message
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_drive_file_permissions" , is_read_only = True , service_type = "drive"
)
2025-08-19 14:15:04 -04:00
@require_google_service ( "drive" , "drive_read" )
async def get_drive_file_permissions (
service ,
user_google_email : str ,
file_id : str ,
) -> str :
"""
Gets detailed metadata about a Google Drive file including sharing permissions.
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
Args:
user_google_email (str): The user's Google email address. Required.
file_id (str): The ID of the file to check permissions for.
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
Returns:
str: Detailed file metadata including sharing status and URLs.
"""
2025-12-13 13:49:28 -08:00
logger . info (
f "[get_drive_file_permissions] Checking file { file_id } for { user_google_email } "
)
2025-11-11 16:43:00 +08:00
resolved_file_id , _ = await resolve_drive_item ( service , file_id )
file_id = resolved_file_id
2025-11-25 13:36:02 +08:00
2025-08-19 14:15:04 -04:00
try :
# Get comprehensive file metadata including permissions
file_metadata = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. get (
2025-08-19 14:15:04 -04:00
fileId = file_id ,
fields = "id, name, mimeType, size, modifiedTime, owners, permissions, "
2025-12-13 13:49:28 -08:00
"webViewLink, webContentLink, shared, sharingUser, viewersCanCopyContent" ,
supportsAllDrives = True ,
)
. execute
2025-08-19 14:15:04 -04:00
)
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Format the response
output_parts = [
f "File: { file_metadata . get ( 'name' , 'Unknown' ) } " ,
f "ID: { file_id } " ,
f "Type: { file_metadata . get ( 'mimeType' , 'Unknown' ) } " ,
f "Size: { file_metadata . get ( 'size' , 'N/A' ) } bytes" ,
f "Modified: { file_metadata . get ( 'modifiedTime' , 'N/A' ) } " ,
"" ,
"Sharing Status:" ,
f " Shared: { file_metadata . get ( 'shared' , False ) } " ,
]
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Add sharing user if available
2025-12-13 13:49:28 -08:00
sharing_user = file_metadata . get ( "sharingUser" )
2025-08-19 14:15:04 -04:00
if sharing_user :
2025-12-13 13:49:28 -08:00
output_parts . append (
f " Shared by: { sharing_user . get ( 'displayName' , 'Unknown' ) } ( { sharing_user . get ( 'emailAddress' , 'Unknown' ) } )"
)
2025-08-19 14:15:04 -04:00
# Process permissions
2025-12-13 13:49:28 -08:00
permissions = file_metadata . get ( "permissions" , [])
2025-08-19 14:15:04 -04:00
if permissions :
output_parts . append ( f " Number of permissions: { len ( permissions ) } " )
output_parts . append ( " Permissions:" )
for perm in permissions :
2025-12-13 13:49:28 -08:00
perm_type = perm . get ( "type" , "unknown" )
role = perm . get ( "role" , "unknown" )
if perm_type == "anyone" :
2025-08-19 14:15:04 -04:00
output_parts . append ( f " - Anyone with the link ( { role } )" )
2025-12-13 13:49:28 -08:00
elif perm_type == "user" :
email = perm . get ( "emailAddress" , "unknown" )
2025-08-19 14:15:04 -04:00
output_parts . append ( f " - User: { email } ( { role } )" )
2025-12-13 13:49:28 -08:00
elif perm_type == "domain" :
domain = perm . get ( "domain" , "unknown" )
2025-08-19 14:15:04 -04:00
output_parts . append ( f " - Domain: { domain } ( { role } )" )
2025-12-13 13:49:28 -08:00
elif perm_type == "group" :
email = perm . get ( "emailAddress" , "unknown" )
2025-08-19 14:15:04 -04:00
output_parts . append ( f " - Group: { email } ( { role } )" )
else :
output_parts . append ( f " - { perm_type } ( { role } )" )
else :
output_parts . append ( " No additional permissions (private file)" )
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Add URLs
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
"" ,
"URLs:" ,
f " View Link: { file_metadata . get ( 'webViewLink' , 'N/A' ) } " ,
]
)
2025-08-19 14:15:04 -04:00
# webContentLink is only available for files that can be downloaded
2025-12-13 13:49:28 -08:00
web_content_link = file_metadata . get ( "webContentLink" )
2025-08-19 14:15:04 -04:00
if web_content_link :
output_parts . append ( f " Direct Download Link: { web_content_link } " )
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Check if file has "anyone with link" permission
from gdrive.drive_helpers import check_public_link_permission
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
has_public_link = check_public_link_permission ( permissions )
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
if has_public_link :
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
"" ,
"✅ This file is shared with 'Anyone with the link' - it can be inserted into Google Docs" ,
]
)
2025-08-19 14:15:04 -04:00
else :
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
"" ,
"❌ This file is NOT shared with 'Anyone with the link' - it cannot be inserted into Google Docs" ,
" To fix: Right-click the file in Google Drive → Share → Anyone with the link → Viewer" ,
]
)
2025-08-19 14:15:04 -04:00
return " \n " . join ( output_parts )
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
except Exception as e :
logger . error ( f "Error getting file permissions: { e } " )
return f "Error getting file permissions: { e } "
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"check_drive_file_public_access" , is_read_only = True , service_type = "drive"
)
2025-08-19 14:15:04 -04:00
@require_google_service ( "drive" , "drive_read" )
async def check_drive_file_public_access (
service ,
user_google_email : str ,
file_name : str ,
) -> str :
"""
Searches for a file by name and checks if it has public link sharing enabled.
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
Args:
user_google_email (str): The user's Google email address. Required.
file_name (str): The name of the file to check.
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
Returns:
str: Information about the file's sharing status and whether it can be used in Google Docs.
"""
logger . info ( f "[check_drive_file_public_access] Searching for { file_name } " )
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Search for the file
escaped_name = file_name . replace ( "'" , " \\ '" )
query = f "name = ' { escaped_name } '"
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
list_params = {
"q" : query ,
"pageSize" : 10 ,
"fields" : "files(id, name, mimeType, webViewLink)" ,
"supportsAllDrives" : True ,
"includeItemsFromAllDrives" : True ,
}
2025-12-13 13:49:28 -08:00
results = await asyncio . to_thread ( service . files () . list ( ** list_params ) . execute )
files = results . get ( "files" , [])
2025-08-19 14:15:04 -04:00
if not files :
return f "No file found with name ' { file_name } '"
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
if len ( files ) > 1 :
output_parts = [ f "Found { len ( files ) } files with name ' { file_name } ':" ]
for f in files :
output_parts . append ( f " - { f [ 'name' ] } (ID: { f [ 'id' ] } )" )
output_parts . append ( " \n Checking the first file..." )
output_parts . append ( "" )
else :
output_parts = []
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Check permissions for the first file
2025-12-13 13:49:28 -08:00
file_id = files [ 0 ][ "id" ]
2025-11-11 16:43:00 +08:00
resolved_file_id , _ = await resolve_drive_item ( service , file_id )
file_id = resolved_file_id
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
# Get detailed permissions
file_metadata = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . files ()
. get (
2025-08-19 14:15:04 -04:00
fileId = file_id ,
fields = "id, name, mimeType, permissions, webViewLink, webContentLink, shared" ,
2025-12-13 13:49:28 -08:00
supportsAllDrives = True ,
)
. execute
2025-08-19 14:15:04 -04:00
)
2025-12-13 13:49:28 -08:00
permissions = file_metadata . get ( "permissions" , [])
2025-08-19 14:15:04 -04:00
from gdrive.drive_helpers import check_public_link_permission , get_drive_image_url
2025-12-13 13:49:28 -08:00
2025-08-19 14:15:04 -04:00
has_public_link = check_public_link_permission ( permissions )
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
f "File: { file_metadata [ 'name' ] } " ,
f "ID: { file_id } " ,
f "Type: { file_metadata [ 'mimeType' ] } " ,
f "Shared: { file_metadata . get ( 'shared' , False ) } " ,
"" ,
]
)
2025-08-19 14:15:04 -04:00
if has_public_link :
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
"✅ PUBLIC ACCESS ENABLED - This file can be inserted into Google Docs" ,
f "Use with insert_doc_image_url: { get_drive_image_url ( file_id ) } " ,
]
)
2025-08-19 14:15:04 -04:00
else :
2025-12-13 13:49:28 -08:00
output_parts . extend (
[
"❌ NO PUBLIC ACCESS - Cannot insert into Google Docs" ,
"Fix: Drive → Share → 'Anyone with the link' → 'Viewer'" ,
]
)
2025-10-19 14:14:20 -04:00
return " \n " . join ( output_parts )
2025-09-29 08:23:38 -07:00
@server.tool ()
@handle_http_errors ( "update_drive_file" , is_read_only = False , service_type = "drive" )
@require_google_service ( "drive" , "drive_file" )
async def update_drive_file (
service ,
user_google_email : str ,
file_id : str ,
# File metadata updates
name : Optional [ str ] = None ,
description : Optional [ str ] = None ,
mime_type : Optional [ str ] = None ,
# Folder organization
add_parents : Optional [ str ] = None , # Comma-separated folder IDs to add
remove_parents : Optional [ str ] = None , # Comma-separated folder IDs to remove
# File status
starred : Optional [ bool ] = None ,
trashed : Optional [ bool ] = None ,
# Sharing and permissions
writers_can_share : Optional [ bool ] = None ,
copy_requires_writer_permission : Optional [ bool ] = None ,
# Custom properties
properties : Optional [ dict ] = None , # User-visible custom properties
) -> str :
"""
Updates metadata and properties of a Google Drive file.
Args:
user_google_email (str): The user's Google email address. Required.
file_id (str): The ID of the file to update. Required.
name (Optional[str]): New name for the file.
description (Optional[str]): New description for the file.
mime_type (Optional[str]): New MIME type (note: changing type may require content upload).
add_parents (Optional[str]): Comma-separated folder IDs to add as parents.
remove_parents (Optional[str]): Comma-separated folder IDs to remove from parents.
starred (Optional[bool]): Whether to star/unstar the file.
trashed (Optional[bool]): Whether to move file to/from trash.
writers_can_share (Optional[bool]): Whether editors can share the file.
copy_requires_writer_permission (Optional[bool]): Whether copying requires writer permission.
properties (Optional[dict]): Custom key-value properties for the file.
Returns:
str: Confirmation message with details of the updates applied.
"""
logger . info ( f "[update_drive_file] Updating file { file_id } for { user_google_email } " )
2025-11-11 16:43:00 +08:00
current_file_fields = (
"name, description, mimeType, parents, starred, trashed, webViewLink, "
"writersCanShare, copyRequiresWriterPermission, properties"
)
resolved_file_id , current_file = await resolve_drive_item (
service ,
file_id ,
extra_fields = current_file_fields ,
2025-11-10 10:54:38 -05:00
)
2025-11-11 16:43:00 +08:00
file_id = resolved_file_id
2025-09-29 08:23:38 -07:00
2025-11-10 10:54:38 -05:00
# Build the update body with only specified fields
update_body = {}
if name is not None :
2025-12-13 13:49:28 -08:00
update_body [ "name" ] = name
2025-11-10 10:54:38 -05:00
if description is not None :
2025-12-13 13:49:28 -08:00
update_body [ "description" ] = description
2025-11-10 10:54:38 -05:00
if mime_type is not None :
2025-12-13 13:49:28 -08:00
update_body [ "mimeType" ] = mime_type
2025-11-10 10:54:38 -05:00
if starred is not None :
2025-12-13 13:49:28 -08:00
update_body [ "starred" ] = starred
2025-11-10 10:54:38 -05:00
if trashed is not None :
2025-12-13 13:49:28 -08:00
update_body [ "trashed" ] = trashed
2025-11-10 10:54:38 -05:00
if writers_can_share is not None :
2025-12-13 13:49:28 -08:00
update_body [ "writersCanShare" ] = writers_can_share
2025-11-10 10:54:38 -05:00
if copy_requires_writer_permission is not None :
2025-12-13 13:49:28 -08:00
update_body [ "copyRequiresWriterPermission" ] = copy_requires_writer_permission
2025-11-10 10:54:38 -05:00
if properties is not None :
2025-12-13 13:49:28 -08:00
update_body [ "properties" ] = properties
2025-11-10 10:54:38 -05:00
2025-11-11 16:43:00 +08:00
async def _resolve_parent_arguments ( parent_arg : Optional [ str ]) -> Optional [ str ]:
if not parent_arg :
return None
parent_ids = [ part . strip () for part in parent_arg . split ( "," ) if part . strip ()]
if not parent_ids :
return None
resolved_ids = []
for parent in parent_ids :
resolved_parent = await resolve_folder_id ( service , parent )
resolved_ids . append ( resolved_parent )
return "," . join ( resolved_ids )
resolved_add_parents = await _resolve_parent_arguments ( add_parents )
resolved_remove_parents = await _resolve_parent_arguments ( remove_parents )
2025-11-10 10:54:38 -05:00
# Build query parameters for parent changes
query_params = {
2025-12-13 13:49:28 -08:00
"fileId" : file_id ,
"supportsAllDrives" : True ,
"fields" : "id, name, description, mimeType, parents, starred, trashed, webViewLink, writersCanShare, copyRequiresWriterPermission, properties" ,
2025-11-10 10:54:38 -05:00
}
2025-09-29 08:23:38 -07:00
2025-11-11 16:43:00 +08:00
if resolved_add_parents :
2025-12-13 13:49:28 -08:00
query_params [ "addParents" ] = resolved_add_parents
2025-11-11 16:43:00 +08:00
if resolved_remove_parents :
2025-12-13 13:49:28 -08:00
query_params [ "removeParents" ] = resolved_remove_parents
2025-11-10 10:54:38 -05:00
# Only include body if there are updates
if update_body :
2025-12-13 13:49:28 -08:00
query_params [ "body" ] = update_body
2025-11-10 10:54:38 -05:00
# Perform the update
updated_file = await asyncio . to_thread (
service . files () . update ( ** query_params ) . execute
)
2025-09-29 08:23:38 -07:00
2025-11-10 10:54:38 -05:00
# Build response message
2025-12-13 13:49:28 -08:00
output_parts = [
f "✅ Successfully updated file: { updated_file . get ( 'name' , current_file [ 'name' ]) } "
]
2025-11-10 10:54:38 -05:00
output_parts . append ( f " File ID: { file_id } " )
# Report what changed
changes = []
2025-12-13 13:49:28 -08:00
if name is not None and name != current_file . get ( "name" ):
2025-11-10 10:54:38 -05:00
changes . append ( f " • Name: ' { current_file . get ( 'name' ) } ' → ' { name } '" )
if description is not None :
2025-12-13 13:49:28 -08:00
old_desc_value = current_file . get ( "description" )
2025-11-10 10:54:38 -05:00
new_desc_value = description
2025-12-13 13:49:28 -08:00
should_report_change = ( old_desc_value or "" ) != ( new_desc_value or "" )
2025-11-10 10:54:38 -05:00
if should_report_change :
2025-12-13 13:49:28 -08:00
old_desc_display = (
old_desc_value if old_desc_value not in ( None , "" ) else "(empty)"
)
new_desc_display = (
new_desc_value if new_desc_value not in ( None , "" ) else "(empty)"
)
2025-11-10 10:54:38 -05:00
changes . append ( f " • Description: { old_desc_display } → { new_desc_display } " )
if add_parents :
changes . append ( f " • Added to folder(s): { add_parents } " )
if remove_parents :
changes . append ( f " • Removed from folder(s): { remove_parents } " )
2025-12-13 13:49:28 -08:00
current_starred = current_file . get ( "starred" )
2025-11-10 10:54:38 -05:00
if starred is not None and starred != current_starred :
star_status = "starred" if starred else "unstarred"
changes . append ( f " • File { star_status } " )
2025-12-13 13:49:28 -08:00
current_trashed = current_file . get ( "trashed" )
2025-11-10 10:54:38 -05:00
if trashed is not None and trashed != current_trashed :
trash_status = "moved to trash" if trashed else "restored from trash"
changes . append ( f " • File { trash_status } " )
2025-12-13 13:49:28 -08:00
current_writers_can_share = current_file . get ( "writersCanShare" )
2025-11-10 10:54:38 -05:00
if writers_can_share is not None and writers_can_share != current_writers_can_share :
share_status = "can" if writers_can_share else "cannot"
changes . append ( f " • Writers { share_status } share the file" )
2025-12-13 13:49:28 -08:00
current_copy_requires_writer_permission = current_file . get (
"copyRequiresWriterPermission"
)
if (
copy_requires_writer_permission is not None
and copy_requires_writer_permission != current_copy_requires_writer_permission
):
copy_status = (
"requires" if copy_requires_writer_permission else "doesn't require"
)
2025-11-10 10:54:38 -05:00
changes . append ( f " • Copying { copy_status } writer permission" )
if properties :
changes . append ( f " • Updated custom properties: { properties } " )
if changes :
2025-09-29 08:23:38 -07:00
output_parts . append ( "" )
2025-11-10 10:54:38 -05:00
output_parts . append ( "Changes applied:" )
output_parts . extend ( changes )
else :
output_parts . append ( " (No changes were made)" )
2025-09-29 08:23:38 -07:00
2025-11-10 10:54:38 -05:00
output_parts . append ( "" )
output_parts . append ( f "View file: { updated_file . get ( 'webViewLink' , '#' ) } " )
2025-09-29 08:23:38 -07:00
2025-11-10 10:54:38 -05:00
return " \n " . join ( output_parts )