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