Spaces:
Running
Running
Upload app.py
Browse files
app.py
CHANGED
|
@@ -8,8 +8,9 @@ from huggingface_hub import hf_hub_download, HfApi
|
|
| 8 |
import hashlib
|
| 9 |
import urllib.parse
|
| 10 |
import urllib.request
|
| 11 |
-
import urllib.error #
|
| 12 |
import os
|
|
|
|
| 13 |
import 数据库连接 as db
|
| 14 |
|
| 15 |
from router_users import router as users_router
|
|
@@ -37,121 +38,78 @@ app.add_middleware(
|
|
| 37 |
allow_headers=["*"],
|
| 38 |
)
|
| 39 |
|
|
|
|
| 40 |
app.include_router(users_router)
|
| 41 |
app.include_router(items_router)
|
| 42 |
app.include_router(comments_router)
|
| 43 |
app.include_router(messages_router)
|
| 44 |
app.include_router(wallet_router)
|
| 45 |
-
app.include_router(proxy_router)
|
| 46 |
-
|
| 47 |
-
@app.get("/")
|
| 48 |
-
def read_root():
|
| 49 |
-
return {"status": "ok", "message": "API System Protected & Running"}
|
| 50 |
-
|
| 51 |
-
# 【安全优化】:允许的文件后缀白名单,防挂马
|
| 52 |
-
ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".json", ".zip"}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
| 54 |
@app.post("/api/upload")
|
| 55 |
async def upload_file(file: UploadFile = File(...), file_type: str = Form(...)):
|
| 56 |
-
#
|
| 57 |
-
_, ext = os.path.splitext(file.filename)
|
| 58 |
-
if ext.lower() not in ALLOWED_EXTENSIONS:
|
| 59 |
-
return JSONResponse(status_code=400, content={"error": f"安全拦截:不支持上传 {ext} 格式的文件"})
|
| 60 |
-
|
| 61 |
-
# 限制单次读取文件大小,防止撑爆内存
|
| 62 |
content = await file.read()
|
| 63 |
-
if len(content) > 10 * 1024 * 1024:
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
file_hash = hashlib.md5(content).hexdigest()[:10]
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
dir_mapping = {"avatar": "avatars", "cover": "covers", "tool": "tools", "app": "apps"}
|
| 73 |
-
target_dir = dir_mapping.get(file_type, "others")
|
| 74 |
-
full_path_in_repo = f"{target_dir}/{new_filename}"
|
| 75 |
-
|
| 76 |
-
# 交给底层带锁与异步线程的 db 处理
|
| 77 |
-
db.save_file(full_path_in_repo, content)
|
| 78 |
-
|
| 79 |
-
url = f"https://huggingface.co/datasets/{db.DATASET_REPO_ID}/resolve/main/{target_dir}/{safe_url_filename}"
|
| 80 |
-
return {"status": "success", "url": url, "display_name": file.filename, "hashed_name": new_filename}
|
| 81 |
-
|
| 82 |
-
class ValidateRequest(BaseModel):
|
| 83 |
-
item_id: str
|
| 84 |
-
|
| 85 |
-
@app.post("/api/validate_resource")
|
| 86 |
-
async def validate_resource(req: ValidateRequest):
|
| 87 |
-
items_db = db.load_data("items.json", default_data=[])
|
| 88 |
-
item = next((i for i in items_db if i["id"] == req.item_id), None)
|
| 89 |
-
if not item:
|
| 90 |
-
return JSONResponse(content={"error": "该资源已被原作者删除"}, status_code=404)
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
#
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
return JSONResponse(content={"error": "私有仓库访问失��,可能密匙已失效"}, status_code=400)
|
| 113 |
-
except urllib.error.HTTPError as e:
|
| 114 |
-
# 如果抛出 HTTPError (如 401 Unauthorized 或 404 Not Found),说明 Token 假了或库被删了
|
| 115 |
-
return JSONResponse(content={"error": f"该私有库的访问密匙已失效或仓库已被原作者删除 (HTTP {e.code})"}, status_code=400)
|
| 116 |
-
except Exception:
|
| 117 |
-
return JSONResponse(content={"error": "无法连接到 GitHub 验证仓库有效性"}, status_code=400)
|
| 118 |
-
|
| 119 |
-
# 走到这里说明私有库和密匙都是 100% 有效的,放行!
|
| 120 |
-
return {"status": "success"}
|
| 121 |
-
|
| 122 |
-
# 针对普通公开库的无感探测
|
| 123 |
-
try:
|
| 124 |
-
req_obj = urllib.request.Request(link, method="HEAD", headers=headers)
|
| 125 |
-
with urllib.request.urlopen(req_obj, timeout=5) as response:
|
| 126 |
-
if response.status >= 400:
|
| 127 |
-
return JSONResponse(content={"error": "原作者的 Git 仓库已失效或设为私有"}, status_code=400)
|
| 128 |
-
except Exception:
|
| 129 |
-
return JSONResponse(content={"error": "原作者的 Git 仓库无法访问,链接已失效"}, status_code=400)
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
try:
|
| 136 |
-
api = HfApi()
|
| 137 |
-
exists = api.file_exists(repo_id=db.DATASET_REPO_ID, filename=repo_path, repo_type="dataset", token=hf_token)
|
| 138 |
-
if not exists:
|
| 139 |
-
return JSONResponse(content={"error": "该工作流的 JSON 文件已在云端损坏或丢失"}, status_code=400)
|
| 140 |
-
except Exception:
|
| 141 |
-
pass
|
| 142 |
-
|
| 143 |
-
return {"status": "success"}
|
| 144 |
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
url: str
|
| 147 |
item_id: str
|
| 148 |
account: str
|
| 149 |
|
| 150 |
-
@app.post("/api/
|
| 151 |
-
async def
|
| 152 |
target_url = req_data.url
|
| 153 |
-
if not target_url
|
| 154 |
-
return JSONResponse(content={"error": "无效的
|
| 155 |
|
| 156 |
items_db = db.load_data("items.json", default_data=[])
|
| 157 |
item = next((i for i in items_db if i["id"] == req_data.item_id), None)
|
|
@@ -161,6 +119,7 @@ async def proxy_download(req_data: ProxyDownloadRequest, sql_db: Session = Depen
|
|
| 161 |
price = int(item.get("price", 0))
|
| 162 |
author = item.get("author")
|
| 163 |
|
|
|
|
| 164 |
if price > 0 and req_data.account != author:
|
| 165 |
owned = sql_db.query(Ownership).filter(Ownership.account == req_data.account, Ownership.item_id == req_data.item_id).first()
|
| 166 |
if not owned:
|
|
@@ -170,20 +129,46 @@ async def proxy_download(req_data: ProxyDownloadRequest, sql_db: Session = Depen
|
|
| 170 |
if not hf_token: return JSONResponse(content={"error": "云端环境变量未配置 HF_TOKEN"}, status_code=401)
|
| 171 |
|
| 172 |
try:
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
repo_type="dataset",
|
| 179 |
-
filename=repo_path,
|
| 180 |
-
token=hf_token
|
| 181 |
-
)
|
| 182 |
-
|
| 183 |
-
with open(cached_file_path, "rb") as f:
|
| 184 |
-
content = f.read()
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
except Exception as e:
|
| 189 |
-
return JSONResponse(content={"error": "
|
|
|
|
|
|
|
|
|
| 8 |
import hashlib
|
| 9 |
import urllib.parse
|
| 10 |
import urllib.request
|
| 11 |
+
import urllib.error # 用于捕获 HTTPError 以判断私有库状态
|
| 12 |
import os
|
| 13 |
+
import shutil
|
| 14 |
import 数据库连接 as db
|
| 15 |
|
| 16 |
from router_users import router as users_router
|
|
|
|
| 38 |
allow_headers=["*"],
|
| 39 |
)
|
| 40 |
|
| 41 |
+
# 挂载各个业务域路由
|
| 42 |
app.include_router(users_router)
|
| 43 |
app.include_router(items_router)
|
| 44 |
app.include_router(comments_router)
|
| 45 |
app.include_router(messages_router)
|
| 46 |
app.include_router(wallet_router)
|
| 47 |
+
app.include_router(proxy_router)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
# ==========================================
|
| 50 |
+
# 核心上传接口 (修复:使用 HF Datasets 永久存储多媒体)
|
| 51 |
+
# ==========================================
|
| 52 |
@app.post("/api/upload")
|
| 53 |
async def upload_file(file: UploadFile = File(...), file_type: str = Form(...)):
|
| 54 |
+
# 1. 拦截超大文件 (限制为 10MB)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
content = await file.read()
|
| 56 |
+
if len(content) > 10 * 1024 * 1024:
|
| 57 |
+
raise HTTPException(status_code=400, detail="文件过大,请限制在 10MB 以内")
|
| 58 |
+
|
| 59 |
+
# 2. 生成防木马的安全文件名 (利用 MD5)
|
| 60 |
+
ext = file.filename.split(".")[-1].lower()
|
| 61 |
+
if ext not in ["jpg", "jpeg", "png", "gif", "webp", "json", "mp4"]:
|
| 62 |
+
raise HTTPException(status_code=400, detail="不支持的文件格式")
|
| 63 |
+
|
| 64 |
file_hash = hashlib.md5(content).hexdigest()[:10]
|
| 65 |
+
safe_filename = f"{file_type}_{file_hash}.{ext}"
|
| 66 |
|
| 67 |
+
# 3. 临时存放在 Spaces 容器本地
|
| 68 |
+
local_tmp_path = f"/tmp/{safe_filename}"
|
| 69 |
+
with open(local_tmp_path, "wb") as f:
|
| 70 |
+
f.write(content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
# 4. 🚀 核心修复:直接将文件上传到 Hugging Face Dataset (永久图床/存储)
|
| 73 |
+
hf_token = os.environ.get("HF_TOKEN")
|
| 74 |
+
dataset_repo_id = "ZHIWEI666/ComfyUI-Ranking"
|
| 75 |
|
| 76 |
+
try:
|
| 77 |
+
api = HfApi()
|
| 78 |
+
# 将文件上传到 Dataset 仓库的 uploads/{file_type} 文件夹下
|
| 79 |
+
api.upload_file(
|
| 80 |
+
path_or_fileobj=local_tmp_path,
|
| 81 |
+
path_in_repo=f"uploads/{file_type}/{safe_filename}",
|
| 82 |
+
repo_id=dataset_repo_id,
|
| 83 |
+
repo_type="dataset",
|
| 84 |
+
token=hf_token,
|
| 85 |
+
commit_message=f"Upload media: {safe_filename}"
|
| 86 |
+
)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
raise HTTPException(status_code=500, detail=f"图床同步失败: {str(e)}")
|
| 89 |
+
finally:
|
| 90 |
+
# 清理容器的临时文件,防止爆内存
|
| 91 |
+
if os.path.exists(local_tmp_path):
|
| 92 |
+
os.remove(local_tmp_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
# 5. 返回 Dataset 的永久直链 (利用 HF 的底层 Raw 链接,永不失效且自带 CDN)
|
| 95 |
+
permanent_url = f"https://huggingface.co/datasets/{dataset_repo_id}/resolve/main/uploads/{file_type}/{safe_filename}"
|
| 96 |
+
|
| 97 |
+
return {"status": "success", "url": permanent_url}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
|
| 100 |
+
# ==========================================
|
| 101 |
+
# 资源验证与所有权拦截接口 (原逻辑无损保留)
|
| 102 |
+
# ==========================================
|
| 103 |
+
class ValidateResourceRequest(BaseModel):
|
| 104 |
url: str
|
| 105 |
item_id: str
|
| 106 |
account: str
|
| 107 |
|
| 108 |
+
@app.post("/api/validate_resource")
|
| 109 |
+
async def validate_resource(req_data: ValidateResourceRequest, sql_db: Session = Depends(get_db)):
|
| 110 |
target_url = req_data.url
|
| 111 |
+
if not target_url.startswith("https://huggingface.co/datasets/") and not target_url.startswith("https://github.com/"):
|
| 112 |
+
return JSONResponse(content={"error": "无效的下载链接"}, status_code=400)
|
| 113 |
|
| 114 |
items_db = db.load_data("items.json", default_data=[])
|
| 115 |
item = next((i for i in items_db if i["id"] == req_data.item_id), None)
|
|
|
|
| 119 |
price = int(item.get("price", 0))
|
| 120 |
author = item.get("author")
|
| 121 |
|
| 122 |
+
# 【拦截逻辑】:若不是作者本人,必须去 SQL 库校验所有权
|
| 123 |
if price > 0 and req_data.account != author:
|
| 124 |
owned = sql_db.query(Ownership).filter(Ownership.account == req_data.account, Ownership.item_id == req_data.item_id).first()
|
| 125 |
if not owned:
|
|
|
|
| 129 |
if not hf_token: return JSONResponse(content={"error": "云端环境变量未配置 HF_TOKEN"}, status_code=401)
|
| 130 |
|
| 131 |
try:
|
| 132 |
+
# 情况 1:如果是 GitHub 私有库,探测死链
|
| 133 |
+
if target_url.startswith("https://github.com/"):
|
| 134 |
+
creator_token = item.get("github_token")
|
| 135 |
+
fallback_token = os.environ.get("GITHUB_PAT")
|
| 136 |
+
active_token = creator_token if creator_token else fallback_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
headers = {"User-Agent": "ComfyUI-Ranking-SaaS"}
|
| 139 |
+
if active_token:
|
| 140 |
+
headers["Authorization"] = f"Bearer {active_token}"
|
| 141 |
+
|
| 142 |
+
repo_parts = target_url.rstrip("/").split("/")
|
| 143 |
+
if len(repo_parts) < 2: return JSONResponse(content={"error": "无效的仓库地址格式"}, status_code=400)
|
| 144 |
+
owner, repo = repo_parts[-2], repo_parts[-1]
|
| 145 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}"
|
| 146 |
+
|
| 147 |
+
req = urllib.request.Request(api_url, headers=headers)
|
| 148 |
+
with urllib.request.urlopen(req) as response:
|
| 149 |
+
if response.status != 200:
|
| 150 |
+
return JSONResponse(content={"error": "资源仓库不可访问,可能已被作者删除或设为私有"}, status_code=404)
|
| 151 |
+
return {"status": "success", "message": "资源有效"}
|
| 152 |
+
|
| 153 |
+
# 情况 2:如果是 Hugging Face 文件 (如 JSON 工作流),校验云端文件是否存在
|
| 154 |
+
elif target_url.startswith("https://huggingface.co/datasets/"):
|
| 155 |
+
repo_path_encoded = target_url.split("resolve/main/")[-1]
|
| 156 |
+
repo_path = urllib.parse.unquote(repo_path_encoded)
|
| 157 |
+
|
| 158 |
+
cached_file_path = hf_hub_download(
|
| 159 |
+
repo_id=db.DATASET_REPO_ID,
|
| 160 |
+
repo_type="dataset",
|
| 161 |
+
filename=repo_path,
|
| 162 |
+
token=hf_token
|
| 163 |
+
)
|
| 164 |
+
if not os.path.exists(cached_file_path):
|
| 165 |
+
return JSONResponse(content={"error": "云端文件不存在,可能已被作者删除"}, status_code=404)
|
| 166 |
+
|
| 167 |
+
return {"status": "success", "message": "资源有效"}
|
| 168 |
+
|
| 169 |
+
except urllib.error.HTTPError as e:
|
| 170 |
+
return JSONResponse(content={"error": f"资源探测失败,源站返回: {e.code}。请联系作者处理。"}, status_code=400)
|
| 171 |
except Exception as e:
|
| 172 |
+
return JSONResponse(content={"error": f"资源探测异常: {str(e)}"}, status_code=500)
|
| 173 |
+
|
| 174 |
+
return {"status": "success"}
|