Spaces:
Running
Running
File size: 9,434 Bytes
c00dff0 e87b2e5 be75899 2814e79 85494ee da1038d 22d80b0 94c35d7 6f73fb4 94c35d7 2814e79 be75899 088dffd 85494ee 6f73fb4 f68de17 85494ee d5dffcf be75899 70ebeb9 85494ee be75899 6f73fb4 be75899 088dffd 85494ee 6525f57 be75899 16c304a 2814e79 16c304a da1038d 6f73fb4 58595a3 2814e79 16c304a 58595a3 16c304a 58595a3 16c304a 58595a3 6525f57 da1038d 6525f57 90d7999 6525f57 85494ee 6525f57 85494ee 6525f57 85494ee 16c304a 6525f57 c3d648d 6525f57 94c35d7 85494ee 94c35d7 6525f57 6f73fb4 94c35d7 6525f57 22d80b0 85494ee 22d80b0 85494ee 22d80b0 6525f57 c3d648d 6525f57 22d80b0 6525f57 | 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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | # ⚙️ 后端逻辑/核心服务端.py (Hugging Face Spaces app.py)
from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, JSONResponse, FileResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel
from huggingface_hub import hf_hub_download, HfApi
import hashlib
import urllib.parse
import urllib.request
import urllib.error
import os
import mimetypes
import 数据库连接 as db
from router_users import router as users_router
from router_items import router as items_router
from router_comments import router as comments_router
from router_messages import router as messages_router
from router_wallet import router as wallet_router
from router_proxy import router as proxy_router
from database_sql import init_sql_db, get_db
from models_sql import Ownership
app = FastAPI(title="ComfyUI Ranking Community API")
@app.get("/")
def health_check():
return {"status": "ok", "message": "ComfyUI Ranking API is running perfectly!"}
@app.on_event("startup")
def on_startup():
init_sql_db()
print("关系型数据库加载完毕,金融表同步完成。")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(users_router)
app.include_router(items_router)
app.include_router(comments_router)
app.include_router(messages_router)
app.include_router(wallet_router)
app.include_router(proxy_router)
# ==========================================
# 🟢 私有图床代理中心 (Image Proxy)
# 解决 Private 仓库下,本地客户端报 401 Unauthorized 的终极方案
# ==========================================
@app.get("/api/image_proxy")
def proxy_hf_image(url: str = None, path: str = None):
"""云端图片代理:使用云端的 HF_TOKEN 提取私有图床图片并返回给没有任何权限的本地端"""
# 兼容处理:如果本地发来的是已经被污染的老版本 HF 直链,我们自动将其转换为相对路径
if url and url.startswith("https://huggingface.co/datasets/"):
try:
path = url.split("resolve/main/")[-1]
path = urllib.parse.unquote(path)
except:
raise HTTPException(status_code=400, detail="无效的 HF 原链接格式")
if not path:
raise HTTPException(status_code=400, detail="缺少路径参数")
# 🛡️ 绝对的安全红线:限制只能代理下载图片目录,严禁黑客通过此接口下载 users.json 或账本数据!
allowed_dirs = ["uploads/", "avatars/", "covers/"]
if not any(path.startswith(d) for d in allowed_dirs):
raise HTTPException(status_code=403, detail="非法访问:该接口仅允许代理图片资源")
hf_token = os.environ.get("HF_TOKEN")
dataset_repo_id = db.DATASET_REPO_ID
try:
# hf_hub_download 会自动利用云端容器的缓存,只有第一次会去真实请求 Dataset
cached_file_path = hf_hub_download(
repo_id=dataset_repo_id,
repo_type="dataset",
filename=path,
token=hf_token
)
# 智能识别文件类型 (image/jpeg, image/png 等)
content_type, _ = mimetypes.guess_type(cached_file_path)
return FileResponse(cached_file_path, media_type=content_type or "application/octet-stream")
except Exception as e:
return JSONResponse(content={"error": f"代理获取图片失败: {str(e)}"}, status_code=404)
# ==========================================
# 上传接口 (将返回的 URL 替换为 Proxy 代理链接)
# ==========================================
@app.post("/api/upload")
def upload_file(file: UploadFile = File(...), file_type: str = Form(...)):
content = file.file.read()
# 🟢 动态文件大小风控
max_size = 10 * 1024 * 1024
if file_type == "avatar":
max_size = 2 * 1024 * 1024
elif file_type == "cover":
max_size = 5 * 1024 * 1024
if len(content) > max_size:
raise HTTPException(status_code=400, detail=f"文件过大,{file_type} 类型请限制在 {max_size // (1024*1024)}MB 以内")
ext = file.filename.split(".")[-1].lower()
if ext not in ["jpg", "jpeg", "png", "gif", "webp", "json", "mp4"]:
raise HTTPException(status_code=400, detail="不支持的文件格式")
file_hash = hashlib.md5(content).hexdigest()[:10]
safe_filename = f"{file_type}_{file_hash}.{ext}"
local_tmp_path = f"/tmp/{safe_filename}"
with open(local_tmp_path, "wb") as f:
f.write(content)
hf_token = os.environ.get("HF_TOKEN")
dataset_repo_id = "ZHIWEI666/ComfyUI-Ranking"
try:
api = HfApi()
api.upload_file(
path_or_fileobj=local_tmp_path,
path_in_repo=f"uploads/{file_type}/{safe_filename}",
repo_id=dataset_repo_id,
repo_type="dataset",
token=hf_token,
commit_message=f"Upload media: {safe_filename}"
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"图床同步失败: {str(e)}")
finally:
if os.path.exists(local_tmp_path):
os.remove(local_tmp_path)
# 🚀 核心修复:不再返回暴露隐私且报 401 的 HF 直链,而是返回我们刚写好的 Proxy 代理链接
permanent_url = f"https://zhiwei666-comfyui-ranking-api.hf.space/api/image_proxy?path=uploads/{file_type}/{safe_filename}"
return {"status": "success", "url": permanent_url}
class ValidateResourceRequest(BaseModel):
url: str
item_id: str
account: str
@app.post("/api/validate_resource")
def validate_resource(req_data: ValidateResourceRequest, sql_db: Session = Depends(get_db)):
target_url = req_data.url
if not target_url.startswith("https://huggingface.co/datasets/") and not target_url.startswith("https://github.com/"):
return JSONResponse(content={"error": "无效的下载链接"}, status_code=400)
items_db = db.load_data("items.json", default_data=[])
item = next((i for i in items_db if i["id"] == req_data.item_id), None)
if not item: return JSONResponse(content={"error": "资源不存在或已被删除"}, status_code=404)
price = int(item.get("price", 0))
author = item.get("author")
if price > 0 and req_data.account != author:
owned = sql_db.query(Ownership).filter(Ownership.account == req_data.account, Ownership.item_id == req_data.item_id).first()
if not owned:
return JSONResponse(content={"error": "🚨 非法下载:云端数据库未找到您的购买凭证!"}, status_code=403)
hf_token = os.environ.get("HF_TOKEN")
if not hf_token: return JSONResponse(content={"error": "云端环境变量未配置 HF_TOKEN"}, status_code=401)
try:
if target_url.startswith("https://github.com/"):
creator_token = item.get("github_token")
fallback_token = os.environ.get("GITHUB_PAT")
active_token = creator_token if creator_token else fallback_token
headers = {"User-Agent": "ComfyUI-Ranking-SaaS"}
if active_token:
headers["Authorization"] = f"Bearer {active_token}"
repo_parts = target_url.rstrip("/").split("/")
if len(repo_parts) < 2: return JSONResponse(content={"error": "无效的仓库地址格式"}, status_code=400)
owner, repo = repo_parts[-2], repo_parts[-1]
api_url = f"https://api.github.com/repos/{owner}/{repo}"
req = urllib.request.Request(api_url, headers=headers)
with urllib.request.urlopen(req) as response:
if response.status != 200:
return JSONResponse(content={"error": "资源仓库不可访问,可能已被作者删除或设为私有"}, status_code=404)
return {"status": "success", "message": "资源有效"}
elif target_url.startswith("https://huggingface.co/datasets/"):
repo_path_encoded = target_url.split("resolve/main/")[-1]
repo_path = urllib.parse.unquote(repo_path_encoded)
cached_file_path = hf_hub_download(
repo_id=db.DATASET_REPO_ID,
repo_type="dataset",
filename=repo_path,
token=hf_token
)
if not os.path.exists(cached_file_path):
return JSONResponse(content={"error": "云端文件不存在,可能已被作者删除"}, status_code=404)
return {"status": "success", "message": "资源有效"}
except urllib.error.HTTPError as e:
return JSONResponse(content={"error": f"资源探测失败,源站返回: {e.code}。请联系作者处理。"}, status_code=400)
except Exception as e:
return JSONResponse(content={"error": f"资源探测异常: {str(e)}"}, status_code=500)
return {"status": "success"} |