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 且保留业务文案 | | 后端(异常写法统一) | 删除 `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 | | 后端(全局兜底修正) | 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 文案断言);成功下载分支零改动 | | 前端(错误文案) | 错误提示改为「后端 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 仍未处理(需后续立项或产品决策) ### 9.2 仍未处理(需后续立项或产品决策)
@@ -5,6 +5,7 @@ import ssl
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.task.storage.core.base import BaseStorageAdapter, FtpAdvancedConfig, StorageObject, StorageProtocol 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): class _ImplicitFTP_TLS(ftplib.FTP_TLS):
"""隐式 FTPS(默认端口 990)连接子类:socket 直连即套 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 != "": if host != "":
self.host = host self.host = host
if port > 0: if port > 0:
@@ -47,6 +55,8 @@ class FtpStorageAdapter(BaseStorageAdapter):
def _new_client(self) -> ftplib.FTP_TLS | ftplib.FTP: def _new_client(self) -> ftplib.FTP_TLS | ftplib.FTP:
"""建立连接并登录。FTP 明文 / FTPS(显式或隐式 TLS)按配置选择。""" """建立连接并登录。FTP 明文 / FTPS(显式或隐式 TLS)按配置选择。"""
if not self.config.host:
raise CustomException(msg="FTP 存储源必须配置主机地址")
implicit = False implicit = False
if self.config.protocol == StorageProtocol.FTPS: if self.config.protocol == StorageProtocol.FTPS:
# 隐式 TLS:implicit_tls 开关或 encrypt_type>=3(兼容两种配置写法) # 隐式 TLS:implicit_tls 开关或 encrypt_type>=3(兼容两种配置写法)
@@ -67,7 +67,9 @@ class OssStorageAdapter(BaseStorageAdapter):
# 走 SDK 高层分片上传:普通小文件自动单次上传,大文件按端点分片参数并发分片; # 走 SDK 高层分片上传:普通小文件自动单次上传,大文件按端点分片参数并发分片;
# 流式传输(stream)时 part_size 为超大值,等效单次上传 # 流式传输(stream)时 part_size 为超大值,等效单次上传
part_size, concurrency, _ = self._multipart_settings() 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), oss.PutObjectRequest(bucket=self.bucket_name, key=remote_path),
local_path, local_path,
part_size=part_size, part_size=part_size,
@@ -36,8 +36,8 @@ class SftpStorageAdapter(BaseStorageAdapter):
"""建立并复用 SFTP 连接:首次使用时连接,之后直接复用。""" """建立并复用 SFTP 连接:首次使用时连接,之后直接复用。"""
if self._sftp is not None: if self._sftp is not None:
return self._sftp return self._sftp
if not self.config.username or not self.config.password: if not self.config.host or not self.config.username or not self.config.password:
raise CustomException(msg="SFTP 存储源必须配置用户名与密码") raise CustomException(msg="SFTP 存储源必须配置主机、用户名与密码")
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try: try: