File size: 8,644 Bytes
c00dff0
e87b2e5
be75899
94c35d7
85494ee
 
 
da1038d
22d80b0
94c35d7
5863217
94c35d7
be75899
 
088dffd
 
 
 
85494ee
5863217
f68de17
85494ee
 
d5dffcf
be75899
 
85494ee
 
 
 
 
be75899
 
 
 
 
 
 
 
088dffd
 
 
 
85494ee
5863217
d5dffcf
be75899
d5dffcf
e87b2e5
 
 
 
be75899
da1038d
 
e87b2e5
 
 
 
 
 
da1038d
e87b2e5
 
 
da1038d
90d7999
e87b2e5
90d7999
 
 
cfb2ea0
da1038d
 
 
e87b2e5
da1038d
90d7999
 
22d80b0
 
85494ee
 
94c35d7
85494ee
 
 
 
 
 
 
 
 
 
 
5863217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85494ee
5863217
85494ee
 
 
e87b2e5
85494ee
 
 
 
 
 
 
 
 
 
 
 
e87b2e5
85494ee
 
c3d648d
94c35d7
 
85494ee
 
94c35d7
22d80b0
85494ee
94c35d7
c3d648d
 
22d80b0
85494ee
 
 
 
 
 
 
 
 
 
 
 
 
22d80b0
85494ee
22d80b0
 
c3d648d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22d80b0
e87b2e5
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
# ⚙️ 后端逻辑/核心服务端.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
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  # 【新增】:用于捕获 HTTPError 以判断私有库状态
import os
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.on_event("startup")
def on_startup():
    init_sql_db()
    print("关系型数据库加载完毕,金融表同步完成。")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    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) 

@app.get("/")
def read_root(): 
    return {"status": "ok", "message": "API System Protected & Running"}

# 【安全优化】:允许的文件后缀白名单,防挂马
ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".json", ".zip"}

@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...), file_type: str = Form(...)):
    # 验证后缀名
    _, ext = os.path.splitext(file.filename)
    if ext.lower() not in ALLOWED_EXTENSIONS:
        return JSONResponse(status_code=400, content={"error": f"安全拦截:不支持上传 {ext} 格式的文件"})

    # 限制单次读取文件大小,防止撑爆内存
    content = await file.read()
    if len(content) > 10 * 1024 * 1024: # 10MB 限制
        return JSONResponse(status_code=400, content={"error": "文件超过 10MB 大小限制"})

    file_hash = hashlib.md5(content).hexdigest()[:10]
    
    new_filename = f"{file_hash}{ext.lower()}"
    safe_filename = urllib.parse.quote(file.filename)
    safe_url_filename = f"{file_hash}_{safe_filename}"
    
    dir_mapping = {"avatar": "avatars", "cover": "covers", "tool": "tools", "app": "apps"}
    target_dir = dir_mapping.get(file_type, "others")
    full_path_in_repo = f"{target_dir}/{new_filename}"
    
    # 交给底层带锁与异步线程的 db 处理
    db.save_file(full_path_in_repo, content)
    
    url = f"https://huggingface.co/datasets/{db.DATASET_REPO_ID}/resolve/main/{target_dir}/{safe_url_filename}"
    return {"status": "success", "url": url, "display_name": file.filename, "hashed_name": new_filename}

class ValidateRequest(BaseModel):
    item_id: str

@app.post("/api/validate_resource")
async def validate_resource(req: ValidateRequest):
    items_db = db.load_data("items.json", default_data=[])
    item = next((i for i in items_db if i["id"] == req.item_id), None)
    if not item:
        return JSONResponse(content={"error": "该资源已被原作者删除"}, status_code=404)
        
    link = item.get("link", "")
    itype = item.get("type", "")
    
    if itype.startswith("tool"):
        headers = {'User-Agent': 'Mozilla/5.0'}
        # 【核心升级】:提取该资源绑定的私有库密匙,如果没有则用全局兜底
        github_token = item.get("github_token") or os.environ.get("GITHUB_PAT")
        
        if github_token and link.startswith("https://github.com/"):
            # 针对私有库:调用 GitHub API 带着 Token 进行身份核验探测
            repo_parts = link.rstrip("/").split("/")
            if len(repo_parts) >= 2:
                owner, repo = repo_parts[-2], repo_parts[-1]
                api_link = f"https://api.github.com/repos/{owner}/{repo}"
                headers["Authorization"] = f"Bearer {github_token}"
                headers["Accept"] = "application/vnd.github.v3+json"
                try:
                    req_obj = urllib.request.Request(api_link, method="GET", headers=headers)
                    with urllib.request.urlopen(req_obj, timeout=5) as response:
                        if response.status >= 400:
                            return JSONResponse(content={"error": "私有仓库访问失败,可能密匙已失效"}, status_code=400)
                except urllib.error.HTTPError as e:
                    # 如果抛出 HTTPError (如 401 Unauthorized 或 404 Not Found),说明 Token 假了或库被删了
                    return JSONResponse(content={"error": f"该私有库的访问密匙已失效或仓库已被原作者删除 (HTTP {e.code})"}, status_code=400)
                except Exception:
                    return JSONResponse(content={"error": "无法连接到 GitHub 验证仓库有效性"}, status_code=400)
                
                # 走到这里说明私有库和密匙都是 100% 有效的,放行!
                return {"status": "success"}

        # 针对普通公开库的无感探测
        try:
            req_obj = urllib.request.Request(link, method="HEAD", headers=headers)
            with urllib.request.urlopen(req_obj, timeout=5) as response:
                if response.status >= 400:
                    return JSONResponse(content={"error": "原作者的 Git 仓库已失效或设为私有"}, status_code=400)
        except Exception:
            return JSONResponse(content={"error": "原作者的 Git 仓库无法访问,链接已失效"}, status_code=400)
            
    elif itype.startswith("app"):
        if "resolve/main/" in link:
            repo_path = urllib.parse.unquote(link.split("resolve/main/")[-1])
            hf_token = os.environ.get("HF_TOKEN")
            try:
                api = HfApi()
                exists = api.file_exists(repo_id=db.DATASET_REPO_ID, filename=repo_path, repo_type="dataset", token=hf_token)
                if not exists:
                    return JSONResponse(content={"error": "该工作流的 JSON 文件已在云端损坏或丢失"}, status_code=400)
            except Exception:
                pass 
                
    return {"status": "success"}

class ProxyDownloadRequest(BaseModel):
    url: str
    item_id: str
    account: str

@app.post("/api/proxy_download")
async def proxy_download(req_data: ProxyDownloadRequest, sql_db: Session = Depends(get_db)):
    target_url = req_data.url
    if not target_url or "resolve/main/" not in target_url:
        return JSONResponse(content={"error": "无效的 Hugging Face 下载链接"}, 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:
        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
        )
        
        with open(cached_file_path, "rb") as f:
            content = f.read()
            
        return Response(content=content, media_type="application/json")
        
    except Exception as e:
        return JSONResponse(content={"error": "云端代理读取失败,可能是源文件损坏"}, status_code=500)