li2895 commited on
Commit
edf5ec6
·
1 Parent(s): f17bf8d

fix: 多文件上传只收第一个(form 返回 starlette.UploadFile 与 fastapi 不同类,isinstance 误过滤)

Browse files
Files changed (1) hide show
  1. 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:仅提取 ``*.json``(忽略其它文件),可带子目录。
209
- - 已存在同名账号 → 覆盖写(保留磁盘上的原 fail_reason / cooldown 之外的字段不变
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
- filename = (file.filename or "").split("/")[-1].split("\\")[-1] # 取 basename
220
- raw = await file.read()
221
- if not raw:
222
- raise HTTPException(status_code=400, detail="empty file")
223
-
224
- if filename.endswith(".zip"):
225
- try:
226
- zf = zipfile.ZipFile(BytesIO(raw))
227
- except zipfile.BadZipFile:
228
- raise HTTPException(status_code=400, detail="invalid zip file") from None
229
- files: list[tuple[str, bytes]] = []
230
- for info in zf.infolist():
231
- if info.is_dir():
232
- continue
233
- if info.filename.endswith(".json"):
234
- files.append((Path(info.filename).name, zf.read(info.filename)))
235
- elif filename.endswith(".json"):
236
- files = [(filename, raw)]
237
- else:
 
 
 
 
 
 
 
 
 
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 files:
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