Files
rose-ash/artdag/l1/app/routers/storage.py
giles 1a74d811f7
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Incorporate art-dag-mono repo into artdag/ subfolder
Merges full history from art-dag/mono.git into the monorepo
under the artdag/ directory. Contains: core (DAG engine),
l1 (Celery rendering server), l2 (ActivityPub registry),
common (shared templates/middleware), client (CLI), test (e2e).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

git-subtree-dir: artdag
git-subtree-mainline: 1a179de547
git-subtree-split: 4c2e716558
2026-02-27 09:07:23 +00:00

265 lines
7.7 KiB
Python

"""
Storage provider routes for L1 server.
Manages user storage backends (Pinata, web3.storage, local, etc.)
"""
from typing import Optional, Dict, Any
from fastapi import APIRouter, Request, Depends, HTTPException, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from pydantic import BaseModel
from artdag_common import render
from artdag_common.middleware import wants_html, wants_json
from artdag_common.middleware.auth import UserContext
from ..dependencies import get_database, get_current_user, require_auth, get_templates
from ..services.storage_service import StorageService, STORAGE_PROVIDERS_INFO, VALID_PROVIDER_TYPES
router = APIRouter()
# Import storage_providers module
import storage_providers as sp_module
def get_storage_service():
"""Get storage service instance."""
import database
return StorageService(database, sp_module)
class AddStorageRequest(BaseModel):
provider_type: str
config: Dict[str, Any]
capacity_gb: int = 5
provider_name: Optional[str] = None
class UpdateStorageRequest(BaseModel):
config: Optional[Dict[str, Any]] = None
capacity_gb: Optional[int] = None
is_active: Optional[bool] = None
@router.get("")
async def list_storage(
request: Request,
storage_service: StorageService = Depends(get_storage_service),
ctx: UserContext = Depends(require_auth),
):
"""List user's storage providers. HTML for browsers, JSON for API."""
storages = await storage_service.list_storages(ctx.actor_id)
if wants_json(request):
return {"storages": storages}
# Render HTML template
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "storage/list.html", request,
storages=storages,
user=ctx,
nav_counts=nav_counts,
providers_info=STORAGE_PROVIDERS_INFO,
active_tab="storage",
)
@router.post("")
async def add_storage(
req: AddStorageRequest,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
):
"""Add a storage provider via API."""
ctx = await require_auth(request)
storage_id, error = await storage_service.add_storage(
actor_id=ctx.actor_id,
provider_type=req.provider_type,
config=req.config,
capacity_gb=req.capacity_gb,
provider_name=req.provider_name,
)
if error:
raise HTTPException(400, error)
return {"id": storage_id, "message": "Storage provider added"}
@router.post("/add")
async def add_storage_form(
request: Request,
provider_type: str = Form(...),
provider_name: Optional[str] = Form(None),
description: Optional[str] = Form(None),
capacity_gb: int = Form(5),
api_key: Optional[str] = Form(None),
secret_key: Optional[str] = Form(None),
api_token: Optional[str] = Form(None),
project_id: Optional[str] = Form(None),
project_secret: Optional[str] = Form(None),
access_key: Optional[str] = Form(None),
bucket: Optional[str] = Form(None),
path: Optional[str] = Form(None),
storage_service: StorageService = Depends(get_storage_service),
):
"""Add a storage provider via HTML form."""
ctx = await get_current_user(request)
if not ctx:
return HTMLResponse('<div class="text-red-400">Not authenticated</div>', status_code=401)
# Build config from form
form_data = {
"api_key": api_key,
"secret_key": secret_key,
"api_token": api_token,
"project_id": project_id,
"project_secret": project_secret,
"access_key": access_key,
"bucket": bucket,
"path": path,
}
config, error = storage_service.build_config_from_form(provider_type, form_data)
if error:
return HTMLResponse(f'<div class="text-red-400">{error}</div>')
storage_id, error = await storage_service.add_storage(
actor_id=ctx.actor_id,
provider_type=provider_type,
config=config,
capacity_gb=capacity_gb,
provider_name=provider_name,
description=description,
)
if error:
return HTMLResponse(f'<div class="text-red-400">{error}</div>')
return HTMLResponse(f'''
<div class="text-green-400 mb-2">Storage provider added successfully!</div>
<script>setTimeout(() => window.location.href = '/storage/type/{provider_type}', 1500);</script>
''')
@router.get("/{storage_id}")
async def get_storage(
storage_id: int,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
):
"""Get a specific storage provider."""
ctx = await require_auth(request)
storage = await storage_service.get_storage(storage_id, ctx.actor_id)
if not storage:
raise HTTPException(404, "Storage provider not found")
return storage
@router.patch("/{storage_id}")
async def update_storage(
storage_id: int,
req: UpdateStorageRequest,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
):
"""Update a storage provider."""
ctx = await require_auth(request)
success, error = await storage_service.update_storage(
storage_id=storage_id,
actor_id=ctx.actor_id,
config=req.config,
capacity_gb=req.capacity_gb,
is_active=req.is_active,
)
if error:
raise HTTPException(400, error)
return {"message": "Storage provider updated"}
@router.delete("/{storage_id}")
async def delete_storage(
storage_id: int,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
ctx: UserContext = Depends(require_auth),
):
"""Remove a storage provider."""
success, error = await storage_service.delete_storage(storage_id, ctx.actor_id)
if error:
raise HTTPException(400, error)
if wants_html(request):
return HTMLResponse("")
return {"message": "Storage provider removed"}
@router.post("/{storage_id}/test")
async def test_storage(
storage_id: int,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
):
"""Test storage provider connectivity."""
ctx = await get_current_user(request)
if not ctx:
if wants_html(request):
return HTMLResponse('<span class="text-red-400">Not authenticated</span>', status_code=401)
raise HTTPException(401, "Not authenticated")
success, message = await storage_service.test_storage(storage_id, ctx.actor_id)
if wants_html(request):
color = "green" if success else "red"
return HTMLResponse(f'<span class="text-{color}-400">{message}</span>')
return {"success": success, "message": message}
@router.get("/type/{provider_type}")
async def storage_type_page(
provider_type: str,
request: Request,
storage_service: StorageService = Depends(get_storage_service),
ctx: UserContext = Depends(require_auth),
):
"""Page for managing storage configs of a specific type."""
if provider_type not in STORAGE_PROVIDERS_INFO:
raise HTTPException(404, "Invalid provider type")
storages = await storage_service.list_by_type(ctx.actor_id, provider_type)
provider_info = STORAGE_PROVIDERS_INFO[provider_type]
if wants_json(request):
return {
"provider_type": provider_type,
"provider_info": provider_info,
"storages": storages,
}
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "storage/type.html", request,
provider_type=provider_type,
provider_info=provider_info,
storages=storages,
user=ctx,
nav_counts=nav_counts,
active_tab="storage",
)