Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
485 lines
17 KiB
Python
485 lines
17 KiB
Python
"""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)
|