File size: 4,680 Bytes
ef7075f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import asyncio

from fastapi import APIRouter, Form, HTTPException, Request, Depends

from src.core.config import DEFAULT_PINECONE_KEY, IDX_FACES, IDX_OBJECTS
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()

@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.")

    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}")

    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}")

    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)}