"""对象存储 key 生成。 布局:users/{user_id}/kbs/{kb_id}/original/{rand8}_{safe_filename} users/{user_id}/kbs/{kb_id}/markdown/{rand8}.md 规则(需求 §27 / §23): - 不信任用户文件名:取 basename,剔除路径穿越与控制字符,截断长度。 - 主体是内部 UUID,公共 URL 使用 token,绝不回显此 key。 """ import re import secrets import uuid _SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._\-]+") _MAX_FILENAME_LEN = 120 def sanitize_filename(original: str) -> str: name = original.replace("\\", "/").split("/")[-1] name = _SAFE_NAME_RE.sub("_", name).strip("._") if not name: name = "file" return name[:_MAX_FILENAME_LEN] def original_object_key( *, user_id: str, knowledge_base_id: str, document_id: str, original_filename: str, ) -> str: rand = secrets.token_urlsafe(6) # ~8 字符 safe = sanitize_filename(original_filename) return ( f"users/{user_id}/kbs/{knowledge_base_id}/docs/{document_id}/original/{rand}_{safe}" ) def markdown_object_key( *, user_id: str, knowledge_base_id: str, document_id: str, ) -> str: rand = secrets.token_urlsafe(6) return ( f"users/{user_id}/kbs/{knowledge_base_id}/docs/{document_id}/markdown/{rand}.md" )