diff --git a/AUDIT-REPORT.md b/AUDIT-REPORT.md index 63ccb65d..eccaff6d 100644 --- a/AUDIT-REPORT.md +++ b/AUDIT-REPORT.md @@ -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 仍未处理(需后续立项或产品决策) diff --git a/backend/app/modules/task/storage/core/ftp_ftps_adapter.py b/backend/app/modules/task/storage/core/ftp_ftps_adapter.py index 4962a357..9dfa746f 100644 --- a/backend/app/modules/task/storage/core/ftp_ftps_adapter.py +++ b/backend/app/modules/task/storage/core/ftp_ftps_adapter.py @@ -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(兼容两种配置写法) diff --git a/backend/app/modules/task/storage/core/oss_adapter.py b/backend/app/modules/task/storage/core/oss_adapter.py index 0495c8f4..b7719fdc 100644 --- a/backend/app/modules/task/storage/core/oss_adapter.py +++ b/backend/app/modules/task/storage/core/oss_adapter.py @@ -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, diff --git a/backend/app/modules/task/storage/core/sftp_adapter.py b/backend/app/modules/task/storage/core/sftp_adapter.py index ba425a38..f10c1517 100644 --- a/backend/app/modules/task/storage/core/sftp_adapter.py +++ b/backend/app/modules/task/storage/core/sftp_adapter.py @@ -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: