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