fix: 多文件上传只收第一个(form 返回 starlette.UploadFile 与 fastapi 不同类,isinstance 误过滤)
Browse files- app/admin.py +33 -27
app/admin.py
CHANGED
|
@@ -199,54 +199,60 @@ async def create_or_update_account(
|
|
| 199 |
@router.post("/accounts/upload")
|
| 200 |
async def upload_accounts(
|
| 201 |
request: Request,
|
| 202 |
-
file: UploadFile = File(...),
|
| 203 |
_: None = Depends(verify_admin_key),
|
| 204 |
) -> dict[str, Any]:
|
| 205 |
"""上传账号文件(.json 单文件,或 .zip 批量),写入 account_dir 并热加载进池。
|
| 206 |
|
| 207 |
- json:文件内须含 ``name`` 字段,文件名须以 .json 结尾(防止误传)。
|
| 208 |
-
- zip:仅提取 ``*.
|
| 209 |
-
- 已存在同名账号 → 覆盖写(
|
| 210 |
-
—— 直接整体覆盖,与 /admin/accounts POST 行为一致)。
|
| 211 |
- 成功后触发热加载:新号进池 + provider 注入,无需重启或手动 /admin/reload。
|
| 212 |
- 凭据只写 account_dir,绝不进日志(http_log 已对上传 body 脱敏)。
|
|
|
|
|
|
|
| 213 |
"""
|
| 214 |
pool = _pool(request)
|
| 215 |
settings = _settings(request)
|
| 216 |
http_client = _http_client(request)
|
| 217 |
providers = _providers(request)
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
raise HTTPException(status_code=400, detail="仅支持 .json 或 .zip 文件")
|
| 239 |
|
| 240 |
-
if not files:
|
| 241 |
-
raise HTTPException(status_code=400, detail="zip 中没有 .json 账号文件")
|
| 242 |
-
|
| 243 |
# 写盘目录 = 池的目录(与 pool.reload() 扫描一致;settings.account_dir 在
|
| 244 |
# 构造池后才改会不一致,测试/运维均以池目录为准)。
|
| 245 |
account_dir = pool.account_dir
|
| 246 |
account_dir.mkdir(parents=True, exist_ok=True)
|
| 247 |
saved: list[str] = []
|
| 248 |
errors: list[dict[str, str]] = []
|
| 249 |
-
for name, content in
|
| 250 |
if not _SAFE_NAME_RE.match(name):
|
| 251 |
errors.append({"file": name, "error": "文件名非法(仅允许字母数字 . _ -)"})
|
| 252 |
continue
|
|
|
|
| 199 |
@router.post("/accounts/upload")
|
| 200 |
async def upload_accounts(
|
| 201 |
request: Request,
|
|
|
|
| 202 |
_: None = Depends(verify_admin_key),
|
| 203 |
) -> dict[str, Any]:
|
| 204 |
"""上传账号文件(.json 单文件,或 .zip 批量),写入 account_dir 并热加载进池。
|
| 205 |
|
| 206 |
- json:文件内须含 ``name`` 字段,文件名须以 .json 结尾(防止误传)。
|
| 207 |
+
- zip:仅提取 ``*.zip``(含 json),可带子目录。
|
| 208 |
+
- 已存在同名账号 → 覆盖写(整体覆盖,与 /admin/accounts POST 行为一致)。
|
|
|
|
| 209 |
- 成功后触发热加载:新号进池 + provider 注入,无需重启或手动 /admin/reload。
|
| 210 |
- 凭据只写 account_dir,绝不进日志(http_log 已对上传 body 脱敏)。
|
| 211 |
+
- 多文件:用 ``request.form()`` 读全部同名 ``file`` 字段(fastapi 0.141 的
|
| 212 |
+
``list[UploadFile]`` 依赖对同名多文件有 bug 返回 422,故绕开)。
|
| 213 |
"""
|
| 214 |
pool = _pool(request)
|
| 215 |
settings = _settings(request)
|
| 216 |
http_client = _http_client(request)
|
| 217 |
providers = _providers(request)
|
| 218 |
|
| 219 |
+
form = await request.form()
|
| 220 |
+
# 注意:form.getlist 返回的是 starlette.datastructures.UploadFile,与
|
| 221 |
+
# fastapi.UploadFile 在 0.141 里 isinstance 不匹配(同名不同类),故不做类型断言。
|
| 222 |
+
uploaded = [v for v in form.getlist("file") if hasattr(v, "read") and hasattr(v, "filename")]
|
| 223 |
+
if not uploaded:
|
| 224 |
+
raise HTTPException(status_code=400, detail="没有收到文件(form 字段名须为 file)")
|
| 225 |
+
|
| 226 |
+
collected: list[tuple[str, bytes]] = []
|
| 227 |
+
for file in uploaded:
|
| 228 |
+
filename = (file.filename or "").split("/")[-1].split("\\")[-1] # 取 basename
|
| 229 |
+
raw = await file.read()
|
| 230 |
+
if not raw:
|
| 231 |
+
continue
|
| 232 |
+
if filename.endswith(".zip"):
|
| 233 |
+
try:
|
| 234 |
+
zf = zipfile.ZipFile(BytesIO(raw))
|
| 235 |
+
except zipfile.BadZipFile:
|
| 236 |
+
raise HTTPException(status_code=400, detail=f"invalid zip file: {filename}") from None
|
| 237 |
+
for info in zf.infolist():
|
| 238 |
+
if info.is_dir():
|
| 239 |
+
continue
|
| 240 |
+
if info.filename.endswith(".json"):
|
| 241 |
+
collected.append((Path(info.filename).name, zf.read(info.filename)))
|
| 242 |
+
elif filename.endswith(".json"):
|
| 243 |
+
collected.append((filename, raw))
|
| 244 |
+
|
| 245 |
+
files_list: list[tuple[str, bytes]] = collected
|
| 246 |
+
if not files_list:
|
| 247 |
raise HTTPException(status_code=400, detail="仅支持 .json 或 .zip 文件")
|
| 248 |
|
|
|
|
|
|
|
|
|
|
| 249 |
# 写盘目录 = 池的目录(与 pool.reload() 扫描一致;settings.account_dir 在
|
| 250 |
# 构造池后才改会不一致,测试/运维均以池目录为准)。
|
| 251 |
account_dir = pool.account_dir
|
| 252 |
account_dir.mkdir(parents=True, exist_ok=True)
|
| 253 |
saved: list[str] = []
|
| 254 |
errors: list[dict[str, str]] = []
|
| 255 |
+
for name, content in files_list:
|
| 256 |
if not _SAFE_NAME_RE.match(name):
|
| 257 |
errors.append({"file": name, "error": "文件名非法(仅允许字母数字 . _ -)"})
|
| 258 |
continue
|