visual-search-api / src /api /explorer.py
AdarshDRC's picture
fix: Resolving backend
29bfc1f
import asyncio
from fastapi import APIRouter, Form, HTTPException, Request, Depends
from src.core.config import (
DEFAULT_PINECONE_KEY, IDX_FACES, IDX_OBJECTS,
IDX_FACES_ARCFACE, IDX_FACES_ADAFACE,
USE_SPLIT_FACE_INDEXES,
)
from src.core.security import get_verified_keys
from src.services.db_client import (
cld_delete_folder_resources, cld_delete_resource, cld_list_folder_images,
cld_remove_folder, cld_root_folders, pinecone_pool,
)
from src.core.logging import log, warn
from src.common.utils import cld_thumb_url, get_ip, url_to_public_id
router = APIRouter()
def _get_face_index_names() -> list[str]:
"""Returns list of face index names to operate on based on current mode."""
if USE_SPLIT_FACE_INDEXES:
# Try both new and legacy — delete from both in case data exists in either
return [IDX_FACES_ARCFACE, IDX_FACES_ADAFACE, IDX_FACES]
return [IDX_FACES]
@router.post("/api/categories")
async def get_categories(
request: Request,
user_id: str = Form(""),
keys: dict = Depends(get_verified_keys),
):
ip = get_ip(request)
try:
result = await asyncio.to_thread(cld_root_folders, keys["cloudinary_creds"])
categories = [f["name"] for f in result.get("folders", [])]
log("INFO", "categories.fetched",
user_id=user_id or "anonymous", ip=ip, count=len(categories))
return {"categories": categories}
except Exception as e:
log("ERROR", "categories.error",
user_id=user_id or "anonymous", ip=ip, error=str(e))
return {"categories": []}
@router.post("/api/cloudinary/folder-images")
async def list_folder_images(
request: Request,
folder_name: str = Form(...),
user_id: str = Form(""),
next_cursor: str = Form(""),
page_size: int = Form(100),
keys: dict = Depends(get_verified_keys),
):
ip = get_ip(request)
result = await asyncio.to_thread(
cld_list_folder_images,
folder_name, keys["cloudinary_creds"], next_cursor or None, page_size,
)
images = [
{
"url": r["secure_url"],
"thumb_url": cld_thumb_url(r["secure_url"]),
"public_id": r["public_id"],
}
for r in result.get("resources", [])
]
next_cur = result.get("next_cursor") or ""
log("INFO", "explorer.folder_opened",
user_id=user_id or "anonymous", ip=ip,
folder=folder_name, count=len(images), has_more=bool(next_cur))
return {"images": images, "count": len(images), "next_cursor": next_cur}
@router.post("/api/delete-image")
async def delete_image(
request: Request,
image_url: str = Form(""),
public_id: str = Form(""),
user_id: str = Form(""),
keys: dict = Depends(get_verified_keys),
):
ip = get_ip(request)
pid = public_id or url_to_public_id(image_url)
if not pid:
raise HTTPException(400, "Could not determine public_id.")
# Delete from Cloudinary
await asyncio.to_thread(cld_delete_resource, pid, keys["cloudinary_creds"])
# Delete from ALL vector indexes (split + legacy + objects)
try:
pc = pinecone_pool.get(keys["pinecone_key"])
existing = {idx.name for idx in pc.list_indexes()}
all_indexes = [IDX_OBJECTS] + _get_face_index_names()
for idx_name in all_indexes:
if idx_name not in existing:
continue
try:
await asyncio.to_thread(
pc.Index(idx_name).delete,
filter={"url": {"$eq": image_url}},
)
except Exception as e:
warn(f"Pinecone delete warning on {idx_name}: {e}")
except Exception as e:
warn(f"Pinecone delete outer warning: {e}")
log("INFO", "explorer.image_deleted",
user_id=user_id or "anonymous", ip=ip,
image_url=image_url, public_id=pid)
return {"message": "Image deleted successfully."}
@router.post("/api/delete-folder")
async def delete_folder(
request: Request,
folder_name: str = Form(...),
user_id: str = Form(""),
keys: dict = Depends(get_verified_keys),
):
ip = get_ip(request)
all_images, cursor = [], None
while True:
result = await asyncio.to_thread(
cld_list_folder_images, folder_name, keys["cloudinary_creds"], cursor
)
all_images.extend(result.get("resources", []))
cursor = result.get("next_cursor")
if not cursor:
break
await asyncio.to_thread(
cld_delete_folder_resources, folder_name, keys["cloudinary_creds"]
)
await asyncio.to_thread(
cld_remove_folder, folder_name, keys["cloudinary_creds"]
)
# Delete from ALL vector indexes
try:
pc = pinecone_pool.get(keys["pinecone_key"])
existing = {idx.name for idx in pc.list_indexes()}
all_indexes = [IDX_OBJECTS] + _get_face_index_names()
for idx_name in all_indexes:
if idx_name not in existing:
continue
idx = pc.Index(idx_name)
try:
# Try metadata filter first (fast)
await asyncio.to_thread(
idx.delete, filter={"folder": {"$eq": folder_name}}
)
except Exception:
# Fallback: delete by URL one-by-one
for img in all_images:
url = img.get("secure_url", "")
if url:
try:
await asyncio.to_thread(
idx.delete, filter={"url": {"$eq": url}}
)
except Exception:
pass
except Exception as e:
warn(f"Pinecone folder delete warning: {e}")
log("INFO", "explorer.folder_deleted",
user_id=user_id or "anonymous", ip=ip,
folder=folder_name, deleted_count=len(all_images))
return {
"message": f"Folder '{folder_name}' and contents deleted.",
"deleted_count": len(all_images),
}