Spaces:
Running
Running
| 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] | |
| 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": []} | |
| 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} | |
| 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."} | |
| 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), | |
| } |