"""Storage Handler - manages Supabase Storage (buckets and files)""" import base64 import json from typing import Any from plugins.supabase.client import SupabaseClient def get_tool_specifications() -> list[dict[str, Any]]: """Return tool specifications for ToolGenerator (12 tools)""" return [ { "name": "list_buckets", "method_name": "list_buckets", "description": "List all storage buckets. Returns bucket names, public/private status, and settings.", "schema": {"type": "object", "properties": {}}, "scope": "read", }, { "name": "get_bucket", "method_name": "get_bucket", "description": "Get detailed information about a specific bucket including settings and limits.", "schema": { "type": "object", "properties": {"bucket_id": {"type": "string", "description": "Bucket name/ID"}}, "required": ["bucket_id"], }, "scope": "read", }, { "name": "create_bucket", "method_name": "create_bucket", "description": "Create a new storage bucket. Can be public (accessible without auth) or private.", "schema": { "type": "object", "properties": { "name": { "type": "string", "description": "Bucket name (lowercase, no spaces)", "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$", }, "public": { "type": "boolean", "description": "Make bucket publicly accessible", "default": False, }, "file_size_limit": { "anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Max file size in bytes (e.g., 52428800 for 50MB)", }, "allowed_mime_types": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Allowed MIME types (e.g., ['image/png', 'image/jpeg'])", }, }, "required": ["name"], }, "scope": "admin", }, { "name": "update_bucket", "method_name": "update_bucket", "description": "Update bucket settings like public access, file size limit, or allowed MIME types.", "schema": { "type": "object", "properties": { "bucket_id": {"type": "string", "description": "Bucket name/ID"}, "public": { "anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Set public access", }, "file_size_limit": { "anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Max file size in bytes", }, "allowed_mime_types": { "anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}], "description": "Allowed MIME types", }, }, "required": ["bucket_id"], }, "scope": "admin", }, { "name": "delete_bucket", "method_name": "delete_bucket", "description": "Delete a storage bucket. Bucket must be empty first (use empty_bucket).", "schema": { "type": "object", "properties": { "bucket_id": {"type": "string", "description": "Bucket name/ID to delete"} }, "required": ["bucket_id"], }, "scope": "admin", }, { "name": "empty_bucket", "method_name": "empty_bucket", "description": "Delete all files in a bucket. Use before delete_bucket.", "schema": { "type": "object", "properties": { "bucket_id": {"type": "string", "description": "Bucket name/ID to empty"} }, "required": ["bucket_id"], }, "scope": "admin", }, { "name": "list_files", "method_name": "list_files", "description": "List files in a bucket or folder path. Returns file names, sizes, and metadata.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name"}, "path": { "type": "string", "description": "Folder path prefix (empty for root)", "default": "", }, "limit": { "type": "integer", "description": "Max files to return", "default": 100, }, "offset": { "type": "integer", "description": "Offset for pagination", "default": 0, }, }, "required": ["bucket"], }, "scope": "read", }, { "name": "upload_file", "method_name": "upload_file", "description": "Upload a file to storage. Content should be base64 encoded.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name"}, "path": { "type": "string", "description": "File path including filename (e.g., 'users/123/avatar.png')", }, "content_base64": { "type": "string", "description": "File content encoded in base64", }, "content_type": { "type": "string", "description": "MIME type (e.g., 'image/png', 'application/pdf')", "default": "application/octet-stream", }, "upsert": { "type": "boolean", "description": "Overwrite if file exists", "default": False, }, }, "required": ["bucket", "path", "content_base64"], }, "scope": "write", }, { "name": "download_file", "method_name": "download_file", "description": "Download a file from storage. Returns base64 encoded content.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name"}, "path": {"type": "string", "description": "File path"}, }, "required": ["bucket", "path"], }, "scope": "read", }, { "name": "delete_files", "method_name": "delete_files", "description": "Delete one or more files from a bucket.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name"}, "paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to delete", }, }, "required": ["bucket", "paths"], }, "scope": "write", }, { "name": "move_file", "method_name": "move_file", "description": "Move or rename a file within the same bucket.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name"}, "from_path": {"type": "string", "description": "Current file path"}, "to_path": {"type": "string", "description": "New file path"}, }, "required": ["bucket", "from_path", "to_path"], }, "scope": "write", }, { "name": "get_public_url", "method_name": "get_public_url", "description": "Get the public URL for a file. Only works for files in public buckets.", "schema": { "type": "object", "properties": { "bucket": {"type": "string", "description": "Bucket name (must be public)"}, "path": {"type": "string", "description": "File path"}, }, "required": ["bucket", "path"], }, "scope": "read", }, ] # ===================== # Storage Operations (12 tools) # ===================== async def list_buckets(client: SupabaseClient) -> str: """List all storage buckets""" try: result = await client.list_buckets() return json.dumps( { "success": True, "count": len(result) if isinstance(result, list) else 0, "buckets": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def get_bucket(client: SupabaseClient, bucket_id: str) -> str: """Get bucket details""" try: result = await client.get_bucket(bucket_id) return json.dumps({"success": True, "bucket": result}, indent=2, ensure_ascii=False) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def create_bucket( client: SupabaseClient, name: str, public: bool = False, file_size_limit: int | None = None, allowed_mime_types: list[str] | None = None, ) -> str: """Create a new bucket""" try: result = await client.create_bucket( name=name, public=public, file_size_limit=file_size_limit, allowed_mime_types=allowed_mime_types, ) return json.dumps( {"success": True, "message": f"Bucket '{name}' created successfully", "bucket": result}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def update_bucket( client: SupabaseClient, bucket_id: str, public: bool | None = None, file_size_limit: int | None = None, allowed_mime_types: list[str] | None = None, ) -> str: """Update bucket settings""" try: result = await client.update_bucket( bucket_id=bucket_id, public=public, file_size_limit=file_size_limit, allowed_mime_types=allowed_mime_types, ) return json.dumps( { "success": True, "message": f"Bucket '{bucket_id}' updated successfully", "bucket": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def delete_bucket(client: SupabaseClient, bucket_id: str) -> str: """Delete a bucket""" try: await client.delete_bucket(bucket_id) return json.dumps( {"success": True, "message": f"Bucket '{bucket_id}' deleted successfully"}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def empty_bucket(client: SupabaseClient, bucket_id: str) -> str: """Empty a bucket (delete all files)""" try: await client.empty_bucket(bucket_id) return json.dumps( {"success": True, "message": f"Bucket '{bucket_id}' emptied successfully"}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def list_files( client: SupabaseClient, bucket: str, path: str = "", limit: int = 100, offset: int = 0 ) -> str: """List files in bucket/path""" try: result = await client.list_files(bucket=bucket, path=path, limit=limit, offset=offset) return json.dumps( { "success": True, "bucket": bucket, "path": path, "count": len(result) if isinstance(result, list) else 0, "files": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def upload_file( client: SupabaseClient, bucket: str, path: str, content_base64: str, content_type: str = "application/octet-stream", upsert: bool = False, ) -> str: """Upload a file""" try: # Decode base64 content try: content = base64.b64decode(content_base64) except Exception: return json.dumps( {"success": False, "error": "Invalid base64 content"}, indent=2, ensure_ascii=False ) result = await client.upload_file( bucket=bucket, path=path, content=content, content_type=content_type, upsert=upsert ) # Build public URL if bucket is public public_url = await client.get_public_url(bucket, path) return json.dumps( { "success": True, "message": f"File uploaded to {bucket}/{path}", "size": len(content), "content_type": content_type, "public_url": public_url, "result": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def download_file(client: SupabaseClient, bucket: str, path: str) -> str: """Download a file (returns base64)""" try: result = await client.download_file(bucket, path) if isinstance(result, dict) and "data" in result: return json.dumps( { "success": True, "bucket": bucket, "path": path, "content_type": result.get("content_type", "application/octet-stream"), "size": result.get("size", 0), "data_base64": result.get("data"), }, indent=2, ensure_ascii=False, ) else: return json.dumps( {"success": True, "bucket": bucket, "path": path, "result": result}, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def delete_files(client: SupabaseClient, bucket: str, paths: list[str]) -> str: """Delete files from bucket""" try: result = await client.delete_files(bucket, paths) return json.dumps( { "success": True, "message": f"Deleted {len(paths)} file(s) from {bucket}", "deleted_paths": paths, "result": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def move_file(client: SupabaseClient, bucket: str, from_path: str, to_path: str) -> str: """Move/rename a file""" try: result = await client.move_file(bucket, from_path, to_path) return json.dumps( { "success": True, "message": f"File moved from {from_path} to {to_path}", "bucket": bucket, "from_path": from_path, "to_path": to_path, "result": result, }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) async def get_public_url(client: SupabaseClient, bucket: str, path: str) -> str: """Get public URL for a file""" try: url = await client.get_public_url(bucket, path) return json.dumps( { "success": True, "bucket": bucket, "path": path, "public_url": url, "note": "URL only works if bucket is public", }, indent=2, ensure_ascii=False, ) except Exception as e: return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)