Spaces:
Sleeping
Sleeping
Create explorer.py
Browse files- src/api/explorer.py +123 -0
src/api/explorer.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Form, HTTPException, Request, Depends
|
| 4 |
+
|
| 5 |
+
from src.core.config import DEFAULT_PINECONE_KEY, IDX_FACES, IDX_OBJECTS
|
| 6 |
+
from src.core.security import get_verified_keys
|
| 7 |
+
from src.services.db_client import (
|
| 8 |
+
cld_delete_folder_resources, cld_delete_resource, cld_list_folder_images,
|
| 9 |
+
cld_remove_folder, cld_root_folders, pinecone_pool,
|
| 10 |
+
)
|
| 11 |
+
from src.core.logging import log, warn
|
| 12 |
+
from src.common.utils import cld_thumb_url, get_ip, url_to_public_id
|
| 13 |
+
|
| 14 |
+
router = APIRouter()
|
| 15 |
+
|
| 16 |
+
@router.post("/api/categories")
|
| 17 |
+
async def get_categories(
|
| 18 |
+
request: Request,
|
| 19 |
+
user_id: str = Form(""),
|
| 20 |
+
keys: dict = Depends(get_verified_keys)
|
| 21 |
+
):
|
| 22 |
+
ip = get_ip(request)
|
| 23 |
+
try:
|
| 24 |
+
result = await asyncio.to_thread(cld_root_folders, keys["cloudinary_creds"])
|
| 25 |
+
categories = [f["name"] for f in result.get("folders", [])]
|
| 26 |
+
log("INFO", "categories.fetched", user_id=user_id or "anonymous", ip=ip, count=len(categories))
|
| 27 |
+
return {"categories": categories}
|
| 28 |
+
except Exception as e:
|
| 29 |
+
log("ERROR", "categories.error", user_id=user_id or "anonymous", ip=ip, error=str(e))
|
| 30 |
+
return {"categories": []}
|
| 31 |
+
|
| 32 |
+
@router.post("/api/cloudinary/folder-images")
|
| 33 |
+
async def list_folder_images(
|
| 34 |
+
request: Request,
|
| 35 |
+
folder_name: str = Form(...),
|
| 36 |
+
user_id: str = Form(""),
|
| 37 |
+
next_cursor: str = Form(""),
|
| 38 |
+
page_size: int = Form(100),
|
| 39 |
+
keys: dict = Depends(get_verified_keys)
|
| 40 |
+
):
|
| 41 |
+
ip = get_ip(request)
|
| 42 |
+
result = await asyncio.to_thread(
|
| 43 |
+
cld_list_folder_images,
|
| 44 |
+
folder_name, keys["cloudinary_creds"], next_cursor or None, page_size,
|
| 45 |
+
)
|
| 46 |
+
images = [
|
| 47 |
+
{
|
| 48 |
+
"url": r["secure_url"],
|
| 49 |
+
"thumb_url": cld_thumb_url(r["secure_url"]),
|
| 50 |
+
"public_id": r["public_id"],
|
| 51 |
+
}
|
| 52 |
+
for r in result.get("resources", [])
|
| 53 |
+
]
|
| 54 |
+
next_cur = result.get("next_cursor") or ""
|
| 55 |
+
log("INFO", "explorer.folder_opened", user_id=user_id or "anonymous", ip=ip,
|
| 56 |
+
folder=folder_name, count=len(images), has_more=bool(next_cur))
|
| 57 |
+
return {"images": images, "count": len(images), "next_cursor": next_cur}
|
| 58 |
+
|
| 59 |
+
@router.post("/api/delete-image")
|
| 60 |
+
async def delete_image(
|
| 61 |
+
request: Request,
|
| 62 |
+
image_url: str = Form(""),
|
| 63 |
+
public_id: str = Form(""),
|
| 64 |
+
user_id: str = Form(""),
|
| 65 |
+
keys: dict = Depends(get_verified_keys)
|
| 66 |
+
):
|
| 67 |
+
ip = get_ip(request)
|
| 68 |
+
pid = public_id or url_to_public_id(image_url)
|
| 69 |
+
if not pid:
|
| 70 |
+
raise HTTPException(400, "Could not determine public_id.")
|
| 71 |
+
|
| 72 |
+
await asyncio.to_thread(cld_delete_resource, pid, keys["cloudinary_creds"])
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
pc = pinecone_pool.get(keys["pinecone_key"])
|
| 76 |
+
for idx_name in [IDX_OBJECTS, IDX_FACES]:
|
| 77 |
+
await asyncio.to_thread(
|
| 78 |
+
pc.Index(idx_name).delete, filter={"url": {"$eq": image_url}}
|
| 79 |
+
)
|
| 80 |
+
except Exception as e:
|
| 81 |
+
warn(f"Pinecone delete warning: {e}")
|
| 82 |
+
|
| 83 |
+
log("INFO", "explorer.image_deleted", user_id=user_id or "anonymous", ip=ip, image_url=image_url, public_id=pid)
|
| 84 |
+
return {"message": "Image deleted successfully."}
|
| 85 |
+
|
| 86 |
+
@router.post("/api/delete-folder")
|
| 87 |
+
async def delete_folder(
|
| 88 |
+
request: Request,
|
| 89 |
+
folder_name: str = Form(...),
|
| 90 |
+
user_id: str = Form(""),
|
| 91 |
+
keys: dict = Depends(get_verified_keys)
|
| 92 |
+
):
|
| 93 |
+
ip = get_ip(request)
|
| 94 |
+
all_images, cursor = [], None
|
| 95 |
+
while True:
|
| 96 |
+
result = await asyncio.to_thread(cld_list_folder_images, folder_name, keys["cloudinary_creds"], cursor)
|
| 97 |
+
all_images.extend(result.get("resources", []))
|
| 98 |
+
cursor = result.get("next_cursor")
|
| 99 |
+
if not cursor:
|
| 100 |
+
break
|
| 101 |
+
|
| 102 |
+
await asyncio.to_thread(cld_delete_folder_resources, folder_name, keys["cloudinary_creds"])
|
| 103 |
+
await asyncio.to_thread(cld_remove_folder, folder_name, keys["cloudinary_creds"])
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
pc = pinecone_pool.get(keys["pinecone_key"])
|
| 107 |
+
for idx_name in [IDX_OBJECTS, IDX_FACES]:
|
| 108 |
+
idx = pc.Index(idx_name)
|
| 109 |
+
try:
|
| 110 |
+
await asyncio.to_thread(idx.delete, filter={"folder": {"$eq": folder_name}})
|
| 111 |
+
except Exception:
|
| 112 |
+
for img in all_images:
|
| 113 |
+
url = img.get("secure_url", "")
|
| 114 |
+
if url:
|
| 115 |
+
try:
|
| 116 |
+
await asyncio.to_thread(idx.delete, filter={"url": {"$eq": url}})
|
| 117 |
+
except Exception:
|
| 118 |
+
pass
|
| 119 |
+
except Exception as e:
|
| 120 |
+
warn(f"Pinecone folder delete warning: {e}")
|
| 121 |
+
|
| 122 |
+
log("INFO", "explorer.folder_deleted", user_id=user_id or "anonymous", ip=ip, folder=folder_name, deleted_count=len(all_images))
|
| 123 |
+
return {"message": f"Folder '{folder_name}' and contents deleted.", "deleted_count": len(all_images)}
|