Skip to content

Commit 4aa8e0c

Browse files
edisonb24claude
andcommitted
Merge PR taylorwilsdon#569: Drive file content update, delete, MIME auto-detect
Adds update_drive_file_content (in-place files.update with media body, preserves file ID), delete_drive_file, and create_drive_file MIME auto-detection. Resolved conflicts against newer upstream main: - union imports (+UserInputError) - kept HEAD base64-capable create_drive_file validation + PR MIME detect - kept PR new tools + HEAD annotated get_drive_shareable_link decorator Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents f50d57d + dd7ab8e commit 4aa8e0c

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

core/tool_tiers.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ drive:
3535
- list_drive_items
3636
- copy_drive_file
3737
- update_drive_file
38+
- update_drive_file_content
39+
- delete_drive_file
3840
- manage_drive_access
3941
- set_drive_file_permissions
4042
complete:

gdrive/drive_tools.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from core.utils import (
2828
GOOGLE_API_WRITE_RETRIES,
2929
IMAGE_MIME_TYPES,
30+
UserInputError,
3031
encode_image_content,
3132
extract_office_xml_text,
3233
extract_pdf_text,
@@ -920,6 +921,26 @@ async def create_drive_file(
920921
f"[create_drive_file] Invoked. Email: '{user_google_email}', File Name: {file_name}, Folder ID: {folder_id}, fileUrl: {fileUrl}"
921922
)
922923

924+
# Auto-detect MIME type from file extension when caller uses the default
925+
if mime_type == "text/plain":
926+
_ext_mime_map = {
927+
".md": "text/markdown",
928+
".markdown": "text/markdown",
929+
".csv": "text/csv",
930+
".html": "text/html",
931+
".htm": "text/html",
932+
".xml": "text/xml",
933+
".json": "application/json",
934+
".yaml": "text/yaml",
935+
".yml": "text/yaml",
936+
}
937+
_ext = Path(file_name).suffix.lower()
938+
if _ext in _ext_mime_map:
939+
mime_type = _ext_mime_map[_ext]
940+
logger.info(
941+
f"[create_drive_file] Auto-detected MIME type '{mime_type}' from extension '{_ext}'"
942+
)
943+
923944
has_existing_content_source = content is not None or bool(fileUrl)
924945
if (
925946
not has_existing_content_source
@@ -1974,6 +1995,190 @@ async def _resolve_parent_arguments(parent_arg: Optional[str]) -> Optional[str]:
19741995
return "\n".join(output_parts)
19751996

19761997

1998+
UPDATABLE_TEXT_MIME_TYPES = {
1999+
"text/plain",
2000+
"text/markdown",
2001+
"text/x-markdown",
2002+
"text/csv",
2003+
"text/html",
2004+
"text/xml",
2005+
"text/yaml",
2006+
"application/json",
2007+
"application/xml",
2008+
"application/x-yaml",
2009+
}
2010+
2011+
2012+
@server.tool()
2013+
@handle_http_errors(
2014+
"update_drive_file_content", is_read_only=False, service_type="drive"
2015+
)
2016+
@require_google_service("drive", "drive_file")
2017+
async def update_drive_file_content(
2018+
service,
2019+
user_google_email: str,
2020+
file_id: str,
2021+
content: str,
2022+
mime_type: Optional[str] = None,
2023+
) -> str:
2024+
"""
2025+
Updates the content of an existing text-based file in Google Drive.
2026+
2027+
Overwrites the entire file content. Works with uploaded text files
2028+
(.md, .txt, .csv, .json, .html, .xml, .yaml). Does NOT work with
2029+
native Google Docs/Sheets/Slides — use modify_doc_text or
2030+
batch_update_doc for those.
2031+
2032+
Args:
2033+
user_google_email (str): The user's Google email address. Required.
2034+
file_id (str): The ID of the file to update. Required.
2035+
content (str): The new file content. Overwrites existing content entirely.
2036+
mime_type (Optional[str]): MIME type for the upload. Defaults to the
2037+
file's existing MIME type. If provided, changes the stored MIME type.
2038+
2039+
Returns:
2040+
str: Confirmation message with file name, ID, character count, and link.
2041+
"""
2042+
logger.info(
2043+
f"[update_drive_file_content] Updating content of {file_id} for {user_google_email}"
2044+
)
2045+
2046+
resolved_file_id, current_file = await resolve_drive_item(
2047+
service,
2048+
file_id,
2049+
extra_fields="name, mimeType, webViewLink",
2050+
)
2051+
file_id = resolved_file_id
2052+
current_mime = current_file.get("mimeType", "text/plain")
2053+
2054+
if current_mime.startswith("application/vnd.google-apps"):
2055+
raise UserInputError(
2056+
f"Cannot update content of native Google file (type: {current_mime}). "
2057+
"Use modify_doc_text or batch_update_doc for Google Docs, "
2058+
"or modify_sheet_values for Google Sheets."
2059+
)
2060+
2061+
if current_mime not in UPDATABLE_TEXT_MIME_TYPES:
2062+
raise UserInputError(
2063+
f"Cannot update content of file with MIME type '{current_mime}'. "
2064+
"Only text-based files are supported: "
2065+
".md, .txt, .csv, .json, .html, .xml, .yaml"
2066+
)
2067+
2068+
if mime_type is not None:
2069+
mime_type = mime_type.strip()
2070+
if not mime_type:
2071+
raise UserInputError("mime_type cannot be empty when provided.")
2072+
if mime_type not in UPDATABLE_TEXT_MIME_TYPES:
2073+
raise UserInputError(
2074+
f"Cannot use MIME type '{mime_type}' for content update. "
2075+
"Only text-based MIME types are supported: "
2076+
+ ", ".join(sorted(UPDATABLE_TEXT_MIME_TYPES))
2077+
)
2078+
content_mime = mime_type
2079+
else:
2080+
content_mime = current_mime
2081+
2082+
media = MediaIoBaseUpload(
2083+
io.BytesIO(content.encode("utf-8")),
2084+
mimetype=content_mime,
2085+
resumable=False,
2086+
)
2087+
2088+
update_kwargs: Dict[str, Any] = {
2089+
"fileId": file_id,
2090+
"media_body": media,
2091+
"fields": "id, name, mimeType, modifiedTime, webViewLink",
2092+
"supportsAllDrives": True,
2093+
}
2094+
if mime_type is not None:
2095+
update_kwargs["body"] = {"mimeType": content_mime}
2096+
2097+
updated_file = await asyncio.to_thread(
2098+
service.files().update(**update_kwargs).execute
2099+
)
2100+
2101+
file_name = updated_file.get("name", current_file.get("name", "unknown"))
2102+
char_count = len(content)
2103+
2104+
output_parts = [
2105+
f"Updated content of '{file_name}' (ID: {file_id}, {char_count} characters)",
2106+
f"Modified: {updated_file.get('modifiedTime', 'unknown')}",
2107+
f"View file: {updated_file.get('webViewLink', '#')}",
2108+
]
2109+
2110+
return "\n".join(output_parts)
2111+
2112+
2113+
@server.tool()
2114+
@handle_http_errors("delete_drive_file", is_read_only=False, service_type="drive")
2115+
@require_google_service("drive", "drive_file")
2116+
async def delete_drive_file(
2117+
service,
2118+
user_google_email: str,
2119+
file_id: str,
2120+
permanent: bool = False,
2121+
) -> str:
2122+
"""
2123+
Deletes a file from Google Drive. Use this tool when asked to delete,
2124+
remove, or trash a file.
2125+
2126+
By default moves the file to trash (recoverable for 30 days).
2127+
Set permanent=True to permanently delete (not recoverable).
2128+
2129+
Args:
2130+
user_google_email (str): The user's Google email address. Required.
2131+
file_id (str): The ID of the file to delete. Required.
2132+
permanent (bool): If True, permanently deletes the file (not recoverable).
2133+
If False (default), moves to trash. If already trashed, returns a
2134+
message indicating the file is already in trash.
2135+
2136+
Returns:
2137+
str: Confirmation message describing what happened.
2138+
"""
2139+
logger.info(
2140+
f"[delete_drive_file] Deleting {file_id} for {user_google_email} (permanent={permanent})"
2141+
)
2142+
2143+
resolved_file_id, current_file = await resolve_drive_item(
2144+
service,
2145+
file_id,
2146+
extra_fields="name, mimeType, trashed",
2147+
)
2148+
file_id = resolved_file_id
2149+
file_name = current_file.get("name", "unknown")
2150+
already_trashed = current_file.get("trashed", False)
2151+
2152+
if permanent:
2153+
await asyncio.to_thread(
2154+
service.files()
2155+
.delete(
2156+
fileId=file_id,
2157+
supportsAllDrives=True,
2158+
)
2159+
.execute
2160+
)
2161+
return f"Permanently deleted '{file_name}' (ID: {file_id})."
2162+
2163+
if already_trashed:
2164+
return (
2165+
f"File '{file_name}' (ID: {file_id}) is already in trash. "
2166+
"Use permanent=True to permanently delete it."
2167+
)
2168+
2169+
await asyncio.to_thread(
2170+
service.files()
2171+
.update(
2172+
fileId=file_id,
2173+
body={"trashed": True},
2174+
fields="id",
2175+
supportsAllDrives=True,
2176+
)
2177+
.execute
2178+
)
2179+
return f"Moved '{file_name}' (ID: {file_id}) to trash. Use permanent=True to permanently delete."
2180+
2181+
19772182
@server.tool(
19782183
title="Get Drive Shareable Link",
19792184
annotations=ToolAnnotations(

0 commit comments

Comments
 (0)