"""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)