Spaces:
Sleeping
Sleeping
fix : Lazy Loading issues
Browse files
main.py
CHANGED
|
@@ -169,8 +169,6 @@ DEFAULT_PC_KEY = os.getenv("DEFAULT_PINECONE_KEY", "")
|
|
| 169 |
DEFAULT_CLD_URL = os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 170 |
|
| 171 |
def _is_default_key(key: str, default: str) -> bool:
|
| 172 |
-
if not key or not key.strip(): # empty key never matches — prevents blocking users with no keys
|
| 173 |
-
return False
|
| 174 |
return bool(default) and key.strip() == default.strip()
|
| 175 |
|
| 176 |
|
|
@@ -739,34 +737,72 @@ async def health():
|
|
| 739 |
# ════════════════════════════════════════════════════════════════
|
| 740 |
# 5. LIST FOLDER IMAGES
|
| 741 |
# ════════════════════════════════════════════════════════════════
|
| 742 |
-
def _cld_list_folder_images(folder: str, creds: dict, next_cursor: str = None):
|
| 743 |
-
kwargs = dict(type="upload", prefix=f"{folder}/", max_results=
|
| 744 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"])
|
| 745 |
if next_cursor: kwargs["next_cursor"] = next_cursor
|
| 746 |
return cloudinary.api.resources(**kwargs)
|
| 747 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
@app.post("/api/cloudinary/folder-images")
|
| 749 |
async def list_folder_images(
|
| 750 |
request: Request,
|
| 751 |
user_cloudinary_url: str = Form(""),
|
| 752 |
folder_name: str = Form(...),
|
| 753 |
user_id: str = Form(""),
|
|
|
|
|
|
|
| 754 |
):
|
| 755 |
ip = get_ip(request)
|
| 756 |
actual_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 757 |
creds = get_cloudinary_creds(actual_url)
|
| 758 |
if not creds.get("cloud_name"): raise HTTPException(400, "Invalid Cloudinary URL.")
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
log("INFO", "explorer.folder_opened",
|
| 767 |
user_id=user_id or "anonymous", ip=ip,
|
| 768 |
-
folder_name=folder_name, image_count=len(images)
|
| 769 |
-
|
|
|
|
| 770 |
|
| 771 |
|
| 772 |
# ════════════════════════════════════════════════════════════════
|
|
@@ -993,39 +1029,34 @@ async def delete_account(
|
|
| 993 |
|
| 994 |
creds = get_cloudinary_creds(user_cloudinary_url)
|
| 995 |
|
| 996 |
-
# ── Cloudinary:
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
_log_fn("WARNING", f"Account delete Cloudinary: {e}")
|
| 1003 |
-
try:
|
| 1004 |
-
folders_res = await asyncio.to_thread(_cld_root_folders, creds)
|
| 1005 |
-
folder_tasks = [
|
| 1006 |
-
asyncio.to_thread(_cld_remove_folder, f["name"], creds)
|
| 1007 |
-
for f in folders_res.get("folders", [])
|
| 1008 |
-
]
|
| 1009 |
-
if folder_tasks:
|
| 1010 |
-
await asyncio.gather(*folder_tasks, return_exceptions=True)
|
| 1011 |
-
except Exception as e:
|
| 1012 |
-
_log_fn("WARNING", f"Account delete Cloudinary folders: {e}")
|
| 1013 |
-
else:
|
| 1014 |
-
_log_fn("INFO", "Account delete: no Cloudinary credentials — skipping CDN wipe")
|
| 1015 |
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
|
| 1030 |
log("WARNING", "danger.delete_account.complete",
|
| 1031 |
user_id=user_id or "anonymous", ip=ip,
|
|
|
|
| 169 |
DEFAULT_CLD_URL = os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 170 |
|
| 171 |
def _is_default_key(key: str, default: str) -> bool:
|
|
|
|
|
|
|
| 172 |
return bool(default) and key.strip() == default.strip()
|
| 173 |
|
| 174 |
|
|
|
|
| 737 |
# ════════════════════════════════════════════════════════════════
|
| 738 |
# 5. LIST FOLDER IMAGES
|
| 739 |
# ════════════════════════════════════════════════════════════════
|
| 740 |
+
def _cld_list_folder_images(folder: str, creds: dict, next_cursor: str = None, max_results: int = 100):
|
| 741 |
+
kwargs = dict(type="upload", prefix=f"{folder}/", max_results=max_results,
|
| 742 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"])
|
| 743 |
if next_cursor: kwargs["next_cursor"] = next_cursor
|
| 744 |
return cloudinary.api.resources(**kwargs)
|
| 745 |
|
| 746 |
+
|
| 747 |
+
def _cld_thumb_url(secure_url: str, cloud_name: str) -> str:
|
| 748 |
+
"""
|
| 749 |
+
Convert full-resolution Cloudinary URL to a small thumbnail URL.
|
| 750 |
+
Inserts Cloudinary transformation: 400×400 fill, auto quality, auto format.
|
| 751 |
+
Example:
|
| 752 |
+
https://res.cloudinary.com/demo/image/upload/v123/folder/img.jpg
|
| 753 |
+
→
|
| 754 |
+
https://res.cloudinary.com/demo/image/upload/w_400,h_400,c_fill,q_auto,f_auto/v123/folder/img.jpg
|
| 755 |
+
"""
|
| 756 |
+
try:
|
| 757 |
+
marker = f"/image/upload/"
|
| 758 |
+
idx = secure_url.find(marker)
|
| 759 |
+
if idx == -1:
|
| 760 |
+
return secure_url # can't transform — return original
|
| 761 |
+
base = secure_url[:idx + len(marker)]
|
| 762 |
+
rest = secure_url[idx + len(marker):]
|
| 763 |
+
# Skip existing transformation block if present
|
| 764 |
+
if rest.startswith("w_") or rest.startswith("h_") or rest.startswith("c_") or rest.startswith("q_"):
|
| 765 |
+
return secure_url # already transformed
|
| 766 |
+
return f"{base}w_400,h_400,c_fill,q_auto,f_auto/{rest}"
|
| 767 |
+
except Exception:
|
| 768 |
+
return secure_url
|
| 769 |
+
|
| 770 |
+
|
| 771 |
@app.post("/api/cloudinary/folder-images")
|
| 772 |
async def list_folder_images(
|
| 773 |
request: Request,
|
| 774 |
user_cloudinary_url: str = Form(""),
|
| 775 |
folder_name: str = Form(...),
|
| 776 |
user_id: str = Form(""),
|
| 777 |
+
next_cursor: str = Form(""),
|
| 778 |
+
page_size: int = Form(100),
|
| 779 |
):
|
| 780 |
ip = get_ip(request)
|
| 781 |
actual_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 782 |
creds = get_cloudinary_creds(actual_url)
|
| 783 |
if not creds.get("cloud_name"): raise HTTPException(400, "Invalid Cloudinary URL.")
|
| 784 |
+
|
| 785 |
+
# Single page fetch — frontend drives pagination via next_cursor
|
| 786 |
+
result = await asyncio.to_thread(
|
| 787 |
+
_cld_list_folder_images, folder_name, creds,
|
| 788 |
+
next_cursor or None, min(page_size, 100)
|
| 789 |
+
)
|
| 790 |
+
images = []
|
| 791 |
+
for r in result.get("resources", []):
|
| 792 |
+
full_url = r["secure_url"]
|
| 793 |
+
thumb_url = _cld_thumb_url(full_url, creds["cloud_name"])
|
| 794 |
+
images.append({
|
| 795 |
+
"url": full_url, # full-res for lightbox / download
|
| 796 |
+
"thumb_url": thumb_url, # 400×400 thumbnail for grid display
|
| 797 |
+
"public_id": r["public_id"],
|
| 798 |
+
})
|
| 799 |
+
|
| 800 |
+
response_cursor = result.get("next_cursor") or ""
|
| 801 |
log("INFO", "explorer.folder_opened",
|
| 802 |
user_id=user_id or "anonymous", ip=ip,
|
| 803 |
+
folder_name=folder_name, image_count=len(images),
|
| 804 |
+
has_more=bool(response_cursor))
|
| 805 |
+
return {"images": images, "count": len(images), "next_cursor": response_cursor}
|
| 806 |
|
| 807 |
|
| 808 |
# ════════════════════════════════════════════════════════════════
|
|
|
|
| 1029 |
|
| 1030 |
creds = get_cloudinary_creds(user_cloudinary_url)
|
| 1031 |
|
| 1032 |
+
# ── Cloudinary: paginated delete ALL resources then folders ────
|
| 1033 |
+
try:
|
| 1034 |
+
deleted = await asyncio.to_thread(_cld_delete_all_paginated, creds)
|
| 1035 |
+
_log_fn("INFO", f"Account delete Cloudinary: {deleted} resources removed")
|
| 1036 |
+
except Exception as e:
|
| 1037 |
+
_log_fn("WARNING", f"Account delete Cloudinary: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1038 |
|
| 1039 |
+
try:
|
| 1040 |
+
folders_res = await asyncio.to_thread(_cld_root_folders, creds)
|
| 1041 |
+
folder_tasks = [
|
| 1042 |
+
asyncio.to_thread(_cld_remove_folder, f["name"], creds)
|
| 1043 |
+
for f in folders_res.get("folders", [])
|
| 1044 |
+
]
|
| 1045 |
+
if folder_tasks:
|
| 1046 |
+
await asyncio.gather(*folder_tasks, return_exceptions=True)
|
| 1047 |
+
except Exception as e:
|
| 1048 |
+
_log_fn("WARNING", f"Account delete Cloudinary folders: {e}")
|
| 1049 |
+
|
| 1050 |
+
# ── Pinecone: delete both indexes ────────────────────────────
|
| 1051 |
+
try:
|
| 1052 |
+
pc = _get_pinecone(user_pinecone_key)
|
| 1053 |
+
existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
|
| 1054 |
+
tasks = []
|
| 1055 |
+
if IDX_OBJECTS in existing: tasks.append(asyncio.to_thread(pc.delete_index, IDX_OBJECTS))
|
| 1056 |
+
if IDX_FACES in existing: tasks.append(asyncio.to_thread(pc.delete_index, IDX_FACES))
|
| 1057 |
+
if tasks: await asyncio.gather(*tasks)
|
| 1058 |
+
except Exception as e:
|
| 1059 |
+
_log_fn("WARNING", f"Account delete Pinecone: {e}")
|
| 1060 |
|
| 1061 |
log("WARNING", "danger.delete_account.complete",
|
| 1062 |
user_id=user_id or "anonymous", ip=ip,
|