"""
Storage Handler - manages Appwrite storage buckets and files
Phase I.3: 14 tools
- Buckets: 5 (list, get, create, update, delete)
- Files: 9 (list, get, create, update, delete, download, preview, view, get_url)
"""
import json
from typing import Any
from plugins.appwrite.client import AppwriteClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (14 tools)"""
return [
# =====================
# BUCKETS (5)
# =====================
{
"name": "list_buckets",
"method_name": "list_buckets",
"description": "List all storage buckets.",
"schema": {
"type": "object",
"properties": {
"queries": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Query strings for filtering",
},
"search": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Search term to filter buckets",
},
},
"required": [],
},
"scope": "read",
},
{
"name": "get_bucket",
"method_name": "get_bucket",
"description": "Get bucket details by ID.",
"schema": {
"type": "object",
"properties": {"bucket_id": {"type": "string", "description": "Bucket ID"}},
"required": ["bucket_id"],
},
"scope": "read",
},
{
"name": "create_bucket",
"method_name": "create_bucket",
"description": "Create a new storage bucket.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {
"type": "string",
"description": "Unique bucket ID. Use 'unique()' for auto-generation",
},
"name": {"type": "string", "description": "Bucket name"},
"permissions": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Bucket permissions",
},
"file_security": {
"type": "boolean",
"description": "Enable file-level security",
"default": True,
},
"enabled": {"type": "boolean", "description": "Enable bucket", "default": True},
"maximum_file_size": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum file size in bytes",
},
"allowed_file_extensions": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Allowed file extensions (e.g., ['jpg', 'png', 'pdf'])",
},
"compression": {
"type": "string",
"enum": ["none", "gzip", "zstd"],
"description": "Compression algorithm",
"default": "none",
},
"encryption": {
"type": "boolean",
"description": "Enable encryption",
"default": True,
},
"antivirus": {
"type": "boolean",
"description": "Enable antivirus scanning",
"default": True,
},
},
"required": ["bucket_id", "name"],
},
"scope": "write",
},
{
"name": "update_bucket",
"method_name": "update_bucket",
"description": "Update bucket settings.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"name": {"type": "string", "description": "Bucket name"},
"permissions": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "New permissions",
},
"enabled": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "Enable/disable bucket",
},
"maximum_file_size": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum file size in bytes",
},
"allowed_file_extensions": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Allowed extensions",
},
},
"required": ["bucket_id", "name"],
},
"scope": "write",
},
{
"name": "delete_bucket",
"method_name": "delete_bucket",
"description": "Delete a storage bucket. All files in the bucket will be deleted.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID to delete"}
},
"required": ["bucket_id"],
},
"scope": "admin",
},
# =====================
# FILES (9)
# =====================
{
"name": "list_files",
"method_name": "list_files",
"description": "List files in a bucket.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"queries": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Query strings for filtering",
},
"search": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Search term to filter files by name",
},
},
"required": ["bucket_id"],
},
"scope": "read",
},
{
"name": "get_file",
"method_name": "get_file",
"description": "Get file metadata (not content).",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID"},
},
"required": ["bucket_id", "file_id"],
},
"scope": "read",
},
{
"name": "delete_file",
"method_name": "delete_file",
"description": "Delete a file from storage.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID to delete"},
},
"required": ["bucket_id", "file_id"],
},
"scope": "write",
},
{
"name": "download_file",
"method_name": "download_file",
"description": "Download file content. Returns base64 encoded data.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID"},
},
"required": ["bucket_id", "file_id"],
},
"scope": "read",
},
{
"name": "get_file_preview",
"method_name": "get_file_preview",
"description": "Get image preview with transformations (resize, crop, etc.). Only works for image files.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID"},
"width": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 4000},
{"type": "null"},
],
"description": "Width in pixels (0-4000)",
},
"height": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 4000},
{"type": "null"},
],
"description": "Height in pixels (0-4000)",
},
"gravity": {
"anyOf": [
{
"type": "string",
"enum": [
"center",
"top",
"top-left",
"top-right",
"left",
"right",
"bottom",
"bottom-left",
"bottom-right",
],
},
{"type": "null"},
],
"description": "Crop gravity",
},
"quality": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 100},
{"type": "null"},
],
"description": "Image quality (0-100)",
},
"border_width": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Border width in pixels",
},
"border_color": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Border color (hex without #)",
},
"border_radius": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Border radius for rounded corners",
},
"rotation": {
"anyOf": [
{"type": "integer", "minimum": 0, "maximum": 360},
{"type": "null"},
],
"description": "Rotation angle (0-360)",
},
"background": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Background color (hex without #)",
},
"output": {
"anyOf": [
{
"type": "string",
"enum": ["jpeg", "jpg", "png", "gif", "webp", "avif"],
},
{"type": "null"},
],
"description": "Output format",
},
},
"required": ["bucket_id", "file_id"],
},
"scope": "read",
},
{
"name": "get_file_view",
"method_name": "get_file_view",
"description": "Get file content for viewing in browser. Returns base64 encoded data.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID"},
},
"required": ["bucket_id", "file_id"],
},
"scope": "read",
},
{
"name": "get_file_url",
"method_name": "get_file_url",
"description": "Get the public URL for a file (if bucket is public).",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_id": {"type": "string", "description": "File ID"},
"url_type": {
"type": "string",
"enum": ["view", "download", "preview"],
"description": "Type of URL to generate",
"default": "view",
},
},
"required": ["bucket_id", "file_id"],
},
"scope": "read",
},
{
"name": "bulk_delete_files",
"method_name": "bulk_delete_files",
"description": "Delete multiple files from a bucket.",
"schema": {
"type": "object",
"properties": {
"bucket_id": {"type": "string", "description": "Bucket ID"},
"file_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of file IDs to delete",
},
},
"required": ["bucket_id", "file_ids"],
},
"scope": "write",
},
{
"name": "get_bucket_stats",
"method_name": "get_bucket_stats",
"description": "Get storage statistics for a bucket (file count and total size).",
"schema": {
"type": "object",
"properties": {"bucket_id": {"type": "string", "description": "Bucket ID"}},
"required": ["bucket_id"],
},
"scope": "read",
},
]
# =====================
# HANDLER FUNCTIONS
# =====================
async def list_buckets(
client: AppwriteClient, queries: list[str] | None = None, search: str | None = None
) -> str:
"""List all buckets."""
try:
result = await client.list_buckets(queries=queries, search=search)
buckets = result.get("buckets", [])
response = {"success": True, "total": result.get("total", len(buckets)), "buckets": buckets}
return json.dumps(response, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_bucket(client: AppwriteClient, bucket_id: str) -> str:
"""Get bucket by ID."""
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)
async def create_bucket(
client: AppwriteClient,
bucket_id: str,
name: str,
permissions: list[str] | None = None,
file_security: bool = True,
enabled: bool = True,
maximum_file_size: int | None = None,
allowed_file_extensions: list[str] | None = None,
compression: str = "none",
encryption: bool = True,
antivirus: bool = True,
) -> str:
"""Create a new bucket."""
try:
result = await client.create_bucket(
bucket_id=bucket_id,
name=name,
permissions=permissions,
file_security=file_security,
enabled=enabled,
maximum_file_size=maximum_file_size,
allowed_file_extensions=allowed_file_extensions,
compression=compression,
encryption=encryption,
antivirus=antivirus,
)
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)
async def update_bucket(
client: AppwriteClient,
bucket_id: str,
name: str,
permissions: list[str] | None = None,
enabled: bool | None = None,
maximum_file_size: int | None = None,
allowed_file_extensions: list[str] | None = None,
) -> str:
"""Update bucket."""
try:
result = await client.update_bucket(
bucket_id=bucket_id,
name=name,
permissions=permissions,
enabled=enabled,
maximum_file_size=maximum_file_size,
allowed_file_extensions=allowed_file_extensions,
)
return json.dumps(
{"success": True, "message": "Bucket updated successfully", "bucket": result},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_bucket(client: AppwriteClient, bucket_id: str) -> str:
"""Delete bucket."""
try:
await client.delete_bucket(bucket_id)
return json.dumps(
{"success": True, "message": f"Bucket '{bucket_id}' deleted successfully"}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def list_files(
client: AppwriteClient,
bucket_id: str,
queries: list[str] | None = None,
search: str | None = None,
) -> str:
"""List files in bucket."""
try:
result = await client.list_files(bucket_id=bucket_id, queries=queries, search=search)
files = result.get("files", [])
response = {
"success": True,
"bucket_id": bucket_id,
"total": result.get("total", len(files)),
"files": files,
}
return json.dumps(response, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Get file metadata."""
try:
result = await client.get_file(bucket_id, file_id)
return json.dumps({"success": True, "file": result}, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Delete file."""
try:
await client.delete_file(bucket_id, file_id)
return json.dumps(
{"success": True, "message": f"File '{file_id}' deleted successfully"}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def download_file(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Download file content."""
try:
result = await client.get_file_download(bucket_id, file_id)
return json.dumps(
{
"success": True,
"file_id": file_id,
"content_type": result.get("content_type"),
"size": result.get("size"),
"data_base64": result.get("data"),
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_file_preview(
client: AppwriteClient,
bucket_id: str,
file_id: str,
width: int | None = None,
height: int | None = None,
gravity: str | None = None,
quality: int | None = None,
border_width: int | None = None,
border_color: str | None = None,
border_radius: int | None = None,
rotation: int | None = None,
background: str | None = None,
output: str | None = None,
) -> str:
"""Get file preview with transformations."""
try:
result = await client.get_file_preview(
bucket_id=bucket_id,
file_id=file_id,
width=width,
height=height,
gravity=gravity,
quality=quality,
border_width=border_width,
border_color=border_color,
border_radius=border_radius,
rotation=rotation,
background=background,
output=output,
)
return json.dumps(
{
"success": True,
"file_id": file_id,
"transformations": {
"width": width,
"height": height,
"quality": quality,
"output": output,
},
"content_type": result.get("content_type"),
"size": result.get("size"),
"data_base64": result.get("data"),
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_file_view(client: AppwriteClient, bucket_id: str, file_id: str) -> str:
"""Get file for viewing."""
try:
result = await client.get_file_view(bucket_id, file_id)
return json.dumps(
{
"success": True,
"file_id": file_id,
"content_type": result.get("content_type"),
"size": result.get("size"),
"data_base64": result.get("data"),
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_file_url(
client: AppwriteClient, bucket_id: str, file_id: str, url_type: str = "view"
) -> str:
"""Get file URL."""
try:
base_url = client.base_url
if url_type == "download":
url = f"{base_url}/storage/buckets/{bucket_id}/files/{file_id}/download"
elif url_type == "preview":
url = f"{base_url}/storage/buckets/{bucket_id}/files/{file_id}/preview"
else:
url = f"{base_url}/storage/buckets/{bucket_id}/files/{file_id}/view"
return json.dumps(
{
"success": True,
"file_id": file_id,
"url_type": url_type,
"url": url,
"note": "URL requires authentication unless bucket is public",
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def bulk_delete_files(client: AppwriteClient, bucket_id: str, file_ids: list[str]) -> str:
"""Delete multiple files."""
try:
results = []
errors = []
for file_id in file_ids:
try:
await client.delete_file(bucket_id, file_id)
results.append({"id": file_id, "success": True})
except Exception as e:
errors.append({"file_id": file_id, "error": str(e)})
response = {
"success": len(errors) == 0,
"deleted": len(results),
"failed": len(errors),
"results": results,
}
if errors:
response["errors"] = errors
return json.dumps(response, indent=2, ensure_ascii=False)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_bucket_stats(client: AppwriteClient, bucket_id: str) -> str:
"""Get bucket statistics."""
try:
# Get files without query params to avoid syntax errors
# Appwrite returns first 25 files by default
result = await client.list_files(bucket_id=bucket_id)
files = result.get("files", [])
total = result.get("total", len(files))
total_size = sum(f.get("sizeOriginal", 0) for f in files)
return json.dumps(
{
"success": True,
"bucket_id": bucket_id,
"stats": {
"file_count": total,
"total_size_bytes": total_size,
"total_size_mb": round(total_size / (1024 * 1024), 2),
"sample_count": len(files),
},
"note": (
f"Size calculated from {len(files)} sampled files"
if total > len(files)
else None
),
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)