@@ -118,7 +118,7 @@ def create_user_to_repo(
118118 :param project: 项目 ID
119119 """
120120 client = self .get_client ()
121- url = urljoin (self .endpoint_url , "/ auth/api/user/create/repo" )
121+ url = safe_urljoin (self .endpoint_url , "auth/api/user/create/repo" )
122122 data = {
123123 "admin" : False ,
124124 "name" : username ,
@@ -134,14 +134,14 @@ def create_user_to_repo(
134134 def update_user (self , username : str , password : str , association_users : List [str ]):
135135 """更新用户信息"""
136136 client = self .get_client ()
137- url = urljoin (self .endpoint_url , f"/ auth/api/user/{ username } " )
137+ url = safe_urljoin (self .endpoint_url , f"auth/api/user/{ username } " )
138138 data = {"admin" : False , "name" : username , "pwd" : password , "asstUsers" : association_users }
139139 return _validate_resp (client .put (url , json = data , timeout = TIMEOUT_THRESHOLD ))
140140
141141 def delete_user (self , username : str ):
142142 """删除用户"""
143143 client = self .get_client ()
144- url = urljoin (self .endpoint_url , f"/ auth/api/user/{ username } " )
144+ url = safe_urljoin (self .endpoint_url , f"auth/api/user/{ username } " )
145145 return _validate_resp (client .delete (url , timeout = TIMEOUT_THRESHOLD ))
146146
147147 def create_repo (self , project : str , repo : str , repo_type : str = RepositoryType .GENERIC , public : bool = False ):
@@ -151,7 +151,7 @@ def create_repo(self, project: str, repo: str, repo_type: str = RepositoryType.G
151151 :param project: 项目 ID
152152 """
153153 client = self .get_client ()
154- url = urljoin (self .endpoint_url , "/ repository/api/repo/create" )
154+ url = safe_urljoin (self .endpoint_url , "repository/api/repo/create" )
155155 data = {
156156 "projectId" : project ,
157157 "name" : repo ,
@@ -172,7 +172,7 @@ def delete_repo(self, project: str, repo: str, forced: bool = False):
172172 :param forced: 是否强制删除, 如果为false,当仓库中存在文件时,将无法删除仓库
173173 """
174174 client = self .get_client ()
175- url = urljoin (self .endpoint_url , f"/ repository/api/repo/delete/{ project } /{ repo } ?forced={ forced } " )
175+ url = safe_urljoin (self .endpoint_url , f"repository/api/repo/delete/{ project } /{ repo } ?forced={ forced } " )
176176 return _validate_resp (client .delete (url , timeout = TIMEOUT_THRESHOLD ))
177177
178178 # 以下是项目无关的管理接口
@@ -183,7 +183,7 @@ def create_project(self, project_name: str):
183183 :param project_name: 项目名称
184184 """
185185 client = self .get_client ()
186- url = urljoin (self .endpoint_url , "/ repository/api/project/create" )
186+ url = safe_urljoin (self .endpoint_url , "repository/api/project/create" )
187187 # Note: 创建项目时传的是项目名称,创建成功后 API 未返回项目 ID 信息
188188 # 按目前 bk-repo 的规则,启用/关闭多租户模式的情况下:
189189 # 关闭多租户: 项目 ID == 项目名称
@@ -200,7 +200,7 @@ def create_user_to_project(self, username: str, password: str, association_users
200200 :param project: 项目 ID
201201 """
202202 client = self .get_client ()
203- url = urljoin (self .endpoint_url , "/ auth/api/user/create/project" )
203+ url = safe_urljoin (self .endpoint_url , "auth/api/user/create/project" )
204204 data = {
205205 "admin" : False ,
206206 "name" : username ,
@@ -260,7 +260,7 @@ def upload_fileobj(self, fh: BinaryIO, key: str, allow_overwrite: bool = True, *
260260 :param allow_overwrite: 是否覆盖已存在文件
261261 """
262262 client = self .get_client ()
263- url = urljoin (self .endpoint_url , f"/ generic/{ self .project } /{ self .bucket } /{ key } " )
263+ url = safe_urljoin (self .endpoint_url , f"generic/{ self .project } /{ self .bucket } /{ key } " )
264264 src = getattr (fh , "name" , "<memory>" )
265265 headers = {"X-BKREPO-OVERWRITE" : str (allow_overwrite )}
266266
@@ -295,7 +295,7 @@ def download_fileobj(self, key: str, fh, *args, **kwargs):
295295 :param fh: 文件句柄
296296 """
297297 client = self .get_client ()
298- url = urljoin (self .endpoint_url , f"/ generic/{ self .project } /{ self .bucket } /{ key } " )
298+ url = safe_urljoin (self .endpoint_url , f"generic/{ self .project } /{ self .bucket } /{ key } " )
299299 dest = getattr (fh , "name" , "<memory>" )
300300 try :
301301 resp = client .get (url , stream = True , timeout = TIMEOUT_THRESHOLD )
@@ -327,7 +327,7 @@ def delete_file(self, key: str, *args, **kwargs):
327327 :param key: 文件完整路径
328328 """
329329 client = self .get_client ()
330- url = urljoin (self .endpoint_url , f"/ generic/{ self .project } /{ self .bucket } /{ key } " )
330+ url = safe_urljoin (self .endpoint_url , f"generic/{ self .project } /{ self .bucket } /{ key } " )
331331 resp = client .delete (url , timeout = TIMEOUT_THRESHOLD )
332332 return _validate_resp (resp )
333333
@@ -337,7 +337,7 @@ def get_file_metadata(self, key, *args, **kwargs):
337337 :param key: 文件完整路径
338338 """
339339 client = self .get_client ()
340- url = urljoin (self .endpoint_url , f"/ generic/{ self .project } /{ self .bucket } /{ key } " )
340+ url = safe_urljoin (self .endpoint_url , f"generic/{ self .project } /{ self .bucket } /{ key } " )
341341 resp = client .head (url , timeout = TIMEOUT_THRESHOLD )
342342 if resp .status_code == 200 :
343343 return resp .headers
@@ -354,7 +354,7 @@ def generate_presigned_url(
354354 :param token_type: [deprecated] token类型。UPLOAD:允许上传, DOWNLOAD: 允许下载, ALL: 同时允许上传和下载。
355355 """
356356 client = self .get_client ()
357- url = urljoin (self .endpoint_url , "/ generic/temporary/url/create" )
357+ url = safe_urljoin (self .endpoint_url , "generic/temporary/url/create" )
358358
359359 token_type = signature_type .value
360360 if "token_type" in kwargs :
@@ -374,3 +374,19 @@ def generate_presigned_url(
374374 )
375375 data = _validate_resp (resp )
376376 return data [0 ]["url" ]
377+
378+
379+ def safe_urljoin (base : str , path : str ) -> str :
380+ """
381+ 安全拼接 URL,自动处理开头/结尾的斜杠。
382+ 即使 path 以 '/' 开头,也会保留 base 的路径。
383+
384+ 示例: "http://example.com/base-path" 和 "/custom-path" 可以正确拼接为 "http://example.com/base-path/custom-path",
385+
386+ :param base: 基础 URL
387+ :param path: 要拼接的路径
388+ """
389+ path = path .lstrip ('/' )
390+ if not base .endswith ('/' ):
391+ base += '/'
392+ return urljoin (base , path )
0 commit comments