Spaces:
Running
Running
File size: 6,077 Bytes
29bfc1f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | 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),
} |