fix(storage): 修复类型检查暴露的三处缺陷(含 OSS 上传运行时 bug)

basedpyright 报出的三处,逐条定位后发现都不是「标注问题」:

1) oss_adapter.py:70 —— 真运行时 bug
   SDK 里 `Client.uploader` 是**方法**(`def uploader(self, **kwargs) -> Uploader`),
   原写法 `self.client.uploader.upload_file(...)` 是在绑定方法上取属性,运行时必抛
   AttributeError —— 即 OSS 上传链路从未成功过(生产未配 OSS 源,故未暴露)。
   已改为 `self.client.uploader().upload_file(...)`,并按 SDK 签名核对
   `upload_file(request, filepath, part_size=..., parallel_num=...)` 参数正确。

2) sftp_adapter.py:45 —— 缺主机校验
   `config.host` 为 `str | None`(对象存储可不填),却直接传给 paramiko
   `connect(hostname: str)`。已把 host 并入既有的用户名/密码守卫,
   缺主机时返回业务异常 400「SFTP 存储源必须配置主机、用户名与密码」。

3) ftp_ftps_adapter.py:58 —— 同上
   `_new_client()` 增加主机守卫(400「FTP 存储源必须配置主机地址」,并补 CustomException 导入);
   顺带修复同文件 `_ImplicitFTP_TLS.connect` 覆写签名与 ftplib.FTP 不兼容
   (timeout: float、source_address: tuple[str, int] | None),消除该文件最后一条 error。

验证:basedpyright 这三个文件 0 errors(修复前 4 条);ruff 全绿;pytest 97 passed;
      运行时探针:缺主机 → 拦截为 400 业务异常;配主机 → 守卫放行并进入真实连接(ConnectionRefused 如期)。
This commit is contained in:
zhangtao
2026-10-04 05:49:27 +08:00
parent a8c992cdd3
commit b3882559de
4 changed files with 17 additions and 4 deletions
+1
View File
@@ -176,6 +176,7 @@
| 后端(异常写法统一) | 删除 `CustomException.internal` 与 132 处包装:意外异常交全局处理器(5xx + 通用文案 + 日志),需运维上下文处 `logger.exception(...) + raise`;6 处 4xx→5xx 反转点补 `except CustomException: raise` | `grep CustomException.internal` = 0;构造器传 status_code 抛 TypeError;AST 审计残留 0;97 用例通过;线上验证码过期仍 400 且保留业务文案 |
| 后端(全局兜底修正) | SQLAlchemyError 非完整性错误由 400 改为 503(连接)/500(其它),文案不再拼 `exc_type` | 读码 + 探针实测 409/503/503/500/500 |
| 前端(错误文案) | 错误提示改为「后端 msg 优先 → 业务码兜底 → 状态码兜底 → 通用」,去掉按 code 白名单取文案的耦合(含 Blob 下载分支;401 排除在解析外以不影响静默续期) | type-check exit 0;vitest 41 用例(含 500+4500 与 404+404 文案断言);成功下载分支零改动 |
| 存储适配器(类型检查暴露的运行时缺陷) | ① OSS:`Client.uploader` 是**方法**,原写法 `self.client.uploader.upload_file(...)` 在绑定方法上取属性 → 运行时 AttributeError(OSS 上传必失败);② SFTP/FTP:`config.host` 可空却直接传给要求 `str` 的 `connect(hostname/host)` | ① 改为 `self.client.uploader().upload_file(...)`(已按 SDK 签名核对 part_size/parallel_num);② 补主机校验,缺主机时返回 400 业务异常;basedpyright 这三个文件由 4 error → 0;运行时探针:缺主机拦截、配主机放行 |
### 9.2 仍未处理(需后续立项或产品决策)
@@ -5,6 +5,7 @@ import ssl
import tempfile
from datetime import datetime
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.modules.task.storage.core.base import BaseStorageAdapter, FtpAdvancedConfig, StorageObject, StorageProtocol
@@ -12,7 +13,14 @@ from app.modules.task.storage.core.base import BaseStorageAdapter, FtpAdvancedCo
class _ImplicitFTP_TLS(ftplib.FTP_TLS):
"""隐式 FTPS(默认端口 990)连接子类:socket 直连即套 TLS。"""
def connect(self, host: str = "", port: int = 0, timeout: int = -999, source_address=None) -> str:
def connect(
self,
host: str = "",
port: int = 0,
timeout: float = -999,
source_address: tuple[str, int] | None = None,
) -> str:
"""覆写 ``ftplib.FTP.connect``:签名与标准库/类型存根保持一致(implicit FTPS 直连即套 TLS)。"""
if host != "":
self.host = host
if port > 0:
@@ -47,6 +55,8 @@ class FtpStorageAdapter(BaseStorageAdapter):
def _new_client(self) -> ftplib.FTP_TLS | ftplib.FTP:
"""建立连接并登录。FTP 明文 / FTPS(显式或隐式 TLS)按配置选择。"""
if not self.config.host:
raise CustomException(msg="FTP 存储源必须配置主机地址")
implicit = False
if self.config.protocol == StorageProtocol.FTPS:
# 隐式 TLS:implicit_tls 开关或 encrypt_type>=3(兼容两种配置写法)
@@ -67,7 +67,9 @@ class OssStorageAdapter(BaseStorageAdapter):
# 走 SDK 高层分片上传:普通小文件自动单次上传,大文件按端点分片参数并发分片;
# 流式传输(stream)时 part_size 为超大值,等效单次上传
part_size, concurrency, _ = self._multipart_settings()
self.client.uploader.upload_file(
# 注意:SDK 的 ``Client.uploader`` 是**方法**(返回 Uploader),必须调用后使用;
# 写成 ``self.client.uploader.upload_file(...)`` 会在运行时抛 AttributeError。
self.client.uploader().upload_file(
oss.PutObjectRequest(bucket=self.bucket_name, key=remote_path),
local_path,
part_size=part_size,
@@ -36,8 +36,8 @@ class SftpStorageAdapter(BaseStorageAdapter):
"""建立并复用 SFTP 连接:首次使用时连接,之后直接复用。"""
if self._sftp is not None:
return self._sftp
if not self.config.username or not self.config.password:
raise CustomException(msg="SFTP 存储源必须配置用户名与密码")
if not self.config.host or not self.config.username or not self.config.password:
raise CustomException(msg="SFTP 存储源必须配置主机、用户名与密码")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try: