Spaces:
Sleeping
Sleeping
File size: 5,394 Bytes
4b61829 | 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 | import asyncio
from fastapi import APIRouter, Form, HTTPException, Request, Depends
from src.core.config import DEFAULT_PINECONE_KEY, IDX_FACES, IDX_OBJECTS, REDIS_TTL_CATEGORIES
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.services.cache import (
cache_get, cache_set, cache_delete_pattern, make_categories_key,
)
from src.core.logging import log, warn
from src.common.utils import cld_thumb_url, get_ip, url_to_public_id
router = APIRouter()
@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:
ck = make_categories_key(keys["cloudinary_creds"].get("cloud_name", ""))
cached = await cache_get(ck)
if cached is not None:
return {"categories": cached}
result = await asyncio.to_thread(cld_root_folders, keys["cloudinary_creds"])
categories = [f["name"] for f in result.get("folders", [])]
await cache_set(ck, categories, ttl=REDIS_TTL_CATEGORIES)
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.")
await asyncio.to_thread(cld_delete_resource, pid, keys["cloudinary_creds"])
try:
pc = pinecone_pool.get(keys["pinecone_key"])
for idx_name in [IDX_OBJECTS, IDX_FACES]:
await asyncio.to_thread(
pc.Index(idx_name).delete, filter={"url": {"$eq": image_url}}
)
except Exception as e:
warn(f"Pinecone delete warning: {e}")
# Invalidate search caches — deleted image may appear in cached results
await cache_delete_pattern("face:*")
await cache_delete_pattern("obj:*")
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"])
try:
pc = pinecone_pool.get(keys["pinecone_key"])
for idx_name in [IDX_OBJECTS, IDX_FACES]:
idx = pc.Index(idx_name)
try:
await asyncio.to_thread(idx.delete, filter={"folder": {"$eq": folder_name}})
except Exception:
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}")
# Invalidate categories and search caches
await cache_delete_pattern(f"cats:*")
await cache_delete_pattern("face:*")
await cache_delete_pattern("obj:*")
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)}
|