Spaces:
Running
Running
Download app/api/transfer.py from a3216/xtc-backend: direct link, hf CLI and curl.
- Browser
- Download file 4.38 kB
-
https://huggingface.co/spaces/a3216/xtc-backend/resolve/main/app/api/transfer.py
- Command line
-
hf download hf://spaces/a3216/xtc-backend/app/api/transfer.py
-
curl -L -o transfer.py https://huggingface.co/spaces/a3216/xtc-backend/resolve/main/app/api/transfer.py
4.38 kB
| """文件中转公开 API:/transfer/api/* | |
| 外部工具凭管理面板生成的「上传密钥」把文件上传到后端中转站: | |
| - POST /transfer/api/upload 上传单个文件(multipart/form-data,字段名 file,可选 alias) | |
| 鉴权:请求头 ``x-xtc-transfer-key: <key>``,或 ``Authorization: Bearer <key>``。 | |
| 不接受 query 传 key(会写进访问日志/浏览器历史,等同泄露长期凭证)。 | |
| 安全设计: | |
| - 单文件大小上限 10MB(超出返回 413),空文件拒绝 | |
| - 存储文件名为服务端生成的随机英文名,原始文件名只作为展示别名(alias) | |
| - 请求路径不在 request_log 记录范围内,避免把文件内容写进请求日志 | |
| - 限流:/transfer/ 前缀走速率限制中间件(see rate_limit_store.is_rate_limited_path) | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| import time | |
| import uuid | |
| from typing import Optional | |
| from fastapi import APIRouter, File, Form, Request, UploadFile | |
| from ..errors import HttpError | |
| from ..services import rate_limit_store, transfer_store | |
| from ._common import ok_with_cors | |
| router = APIRouter(prefix="/transfer/api", tags=["transfer"]) | |
| logger = logging.getLogger(__name__) | |
| MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB | |
| def _extract_key(request: Request) -> str: | |
| k = (request.headers.get("x-xtc-transfer-key") or "").strip() | |
| if k: | |
| return k | |
| auth = (request.headers.get("authorization") or "").strip() | |
| if auth.lower().startswith("bearer "): | |
| return auth[7:].strip() | |
| return "" | |
| def _make_storage_filename(original: str, key: str) -> str: | |
| """根据原始文件名生成安全的英文存储名,保留扩展名。 | |
| 规则:t_{key前8位}_{时间戳后6位}.{ext},例如 "我的报告.pdf" -> "t_a1b2c3d4_8858a1.pdf" | |
| """ | |
| raw = str(original or "").strip() | |
| ext = "" | |
| dot_idx = raw.rfind(".") | |
| if dot_idx > 0: | |
| raw_ext = raw[dot_idx + 1:].lower() | |
| safe_ext = "".join(c for c in raw_ext if c.isalnum())[:8] | |
| if safe_ext: | |
| ext = "." + safe_ext | |
| short_key = key[:8] if key else "00000000" | |
| short_ts = str(int(time.time()))[-6:] | |
| return f"t_{short_key}_{short_ts}{ext}" | |
| async def _require_transfer_key(request: Request) -> str: | |
| key = _extract_key(request) | |
| if not key: | |
| raise HttpError("missing transfer key", status=401, code="unauthorized") | |
| # verify_key 可能触发 Hub 拉取,放线程池避免阻塞事件循环 | |
| valid = await asyncio.to_thread(transfer_store.verify_key, key) | |
| if not valid: | |
| raise HttpError("invalid or revoked transfer key", status=401, code="unauthorized") | |
| return key | |
| async def upload_file( | |
| request: Request, | |
| file: UploadFile = File(...), | |
| alias: Optional[str] = Form(default=None), | |
| ): | |
| """上传文件中转。返回文件 key 与元信息(文件可在管理面板「文件中转」中查看)。""" | |
| key = await _require_transfer_key(request) | |
| content = await file.read() | |
| if not content: | |
| raise HttpError("empty file", status=400, code="bad_request") | |
| if len(content) > MAX_FILE_SIZE: | |
| raise HttpError( | |
| f"file too large: {len(content)} bytes > 10MB ({MAX_FILE_SIZE} bytes)", | |
| status=413, code="too_large", | |
| ) | |
| file_key = uuid.uuid4().hex | |
| display_name = str(alias or "").strip() or str(file.filename or "").strip() or "unnamed" | |
| if len(display_name) > 255: | |
| display_name = display_name[:255] | |
| filename = _make_storage_filename(display_name, file_key) | |
| mime = (file.content_type or "application/octet-stream").strip() or "application/octet-stream" | |
| meta = await transfer_store.save_transfer_file( | |
| key=file_key, | |
| filename=filename, | |
| alias=display_name, | |
| mime=mime, | |
| content=content, | |
| uploader_key=key, | |
| client_ip=rate_limit_store.get_client_ip(request) or "", | |
| ) | |
| logger.info( | |
| "[transfer] upload ok key=%s alias=%s size=%d by_key=%s", | |
| file_key, display_name, meta["size"], transfer_store.mask_key(key), | |
| ) | |
| return ok_with_cors({ | |
| "key": meta["key"], | |
| "filename": meta["filename"], | |
| "alias": meta["alias"], | |
| "mime": meta["mime"], | |
| "size": meta["size"], | |
| "sha256": meta["sha256"], | |
| "uploadedAt": meta["uploaded_at"], | |
| }) | |