From e77530bdc571144a0b0e90d37c8a87c8b8751cf1 Mon Sep 17 00:00:00 2001 From: DDSRem <1448139087@qq.com> Date: Sun, 22 Feb 2026 12:35:27 +0800 Subject: [PATCH] fix(storages): download directory concatenation error --- app/modules/filemanager/storages/alipan.py | 302 +++++++++++---------- app/modules/filemanager/storages/smb.py | 62 +++-- 2 files changed, 194 insertions(+), 170 deletions(-) diff --git a/app/modules/filemanager/storages/alipan.py b/app/modules/filemanager/storages/alipan.py index 5ea152b3..fd6040a7 100644 --- a/app/modules/filemanager/storages/alipan.py +++ b/app/modules/filemanager/storages/alipan.py @@ -38,10 +38,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): schema = StorageSchema.Alipan # 支持的整理方式 - transtype = { - "move": "移动", - "copy": "复制" - } + transtype = {"move": "移动", "copy": "复制"} # 基础url base_url = "https://openapi.alipan.com" @@ -59,9 +56,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): """ 初始化带速率限制的会话 """ - self.session.headers.update({ - "Content-Type": "application/json" - }) + self.session.headers.update({"Content-Type": "application/json"}) def _check_session(self): """ @@ -76,7 +71,11 @@ class AliPan(StorageBase, metaclass=WeakSingleton): 获取默认存储桶ID """ conf = self.get_conf() - drive_id = conf.get("resource_drive_id") or conf.get("backup_drive_id") or conf.get("default_drive_id") + drive_id = ( + conf.get("resource_drive_id") + or conf.get("backup_drive_id") + or conf.get("default_drive_id") + ) if not drive_id: raise NoCheckInException("【阿里云盘】请先扫码登录!") return drive_id @@ -94,10 +93,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): if expires_in and refresh_time + expires_in < int(time.time()): tokens = self.__refresh_access_token(refresh_token) if tokens: - self.set_config({ - "refresh_time": int(time.time()), - **tokens - }) + self.set_config({"refresh_time": int(time.time()), **tokens}) access_token = tokens.get("access_token") if access_token: self.session.headers.update({"Authorization": f"Bearer {access_token}"}) @@ -115,10 +111,15 @@ class AliPan(StorageBase, metaclass=WeakSingleton): f"{self.base_url}/oauth/authorize/qrcode", json={ "client_id": settings.ALIPAN_APP_ID, - "scopes": ["user:base", "file:all:read", "file:all:write", "file:share:write"], + "scopes": [ + "user:base", + "file:all:read", + "file:all:write", + "file:share:write", + ], "code_challenge": code_verifier, - "code_challenge_method": "plain" - } + "code_challenge_method": "plain", + }, ) if resp is None: return {}, "网络错误" @@ -126,14 +127,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): if result.get("code"): return {}, result.get("message") # 持久化验证参数 - self._auth_state = { - "sid": result.get("sid"), - "code_verifier": code_verifier - } + self._auth_state = {"sid": result.get("sid"), "code_verifier": code_verifier} # 生成二维码内容 - return { - "codeUrl": result.get("qrCodeUrl") - }, "" + return {"codeUrl": result.get("qrCodeUrl")}, "" def check_login(self) -> Optional[Tuple[dict, str]]: """ @@ -144,7 +140,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "WaitLogin": "等待登录", "ScanSuccess": "扫码成功", "LoginSuccess": "登录成功", - "QRCodeExpired": "二维码过期" + "QRCodeExpired": "二维码过期", } if not self._auth_state: @@ -163,10 +159,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): self._auth_state["authCode"] = authCode tokens = self.__get_access_token() if tokens: - self.set_config({ - "refresh_time": int(time.time()), - **tokens - }) + self.set_config({"refresh_time": int(time.time()), **tokens}) self.__get_drive_id() return {"status": status, "tip": _status_text.get(status, "未知错误")}, "" except Exception as e: @@ -184,14 +177,16 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "client_id": settings.ALIPAN_APP_ID, "grant_type": "authorization_code", "code": self._auth_state["authCode"], - "code_verifier": self._auth_state["code_verifier"] - } + "code_verifier": self._auth_state["code_verifier"], + }, ) if resp is None: raise SessionInvalidException("【阿里云盘】获取 access_token 失败") result = resp.json() if result.get("code"): - raise Exception(f"【阿里云盘】{result.get('code')} - {result.get('message')}!") + raise Exception( + f"【阿里云盘】{result.get('code')} - {result.get('message')}!" + ) return result def __refresh_access_token(self, refresh_token: str) -> Optional[dict]: @@ -205,30 +200,34 @@ class AliPan(StorageBase, metaclass=WeakSingleton): json={ "client_id": settings.ALIPAN_APP_ID, "grant_type": "refresh_token", - "refresh_token": refresh_token - } + "refresh_token": refresh_token, + }, ) if resp is None: - logger.error(f"【阿里云盘】刷新 access_token 失败:refresh_token={refresh_token}") + logger.error( + f"【阿里云盘】刷新 access_token 失败:refresh_token={refresh_token}" + ) return None result = resp.json() if result.get("code"): - logger.warn(f"【阿里云盘】刷新 access_token 失败:{result.get('code')} - {result.get('message')}!") + logger.warn( + f"【阿里云盘】刷新 access_token 失败:{result.get('code')} - {result.get('message')}!" + ) return result def __get_drive_id(self): """ 获取默认存储桶ID """ - resp = self.session.post( - f"{self.base_url}/adrive/v1.0/user/getDriveInfo" - ) + resp = self.session.post(f"{self.base_url}/adrive/v1.0/user/getDriveInfo") if resp is None: logger.error("获取默认存储桶ID失败") return None result = resp.json() if result.get("code"): - logger.warn(f"获取默认存储ID失败:{result.get('code')} - {result.get('message')}!") + logger.warn( + f"获取默认存储ID失败:{result.get('code')} - {result.get('message')}!" + ) return None # 保存用户参数 """ @@ -244,8 +243,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): self.set_config(conf) return None - def _request_api(self, method: str, endpoint: str, - result_key: Optional[str] = None, **kwargs) -> Optional[Union[dict, list]]: + def _request_api( + self, method: str, endpoint: str, result_key: Optional[str] = None, **kwargs + ) -> Optional[Union[dict, list]]: """ 带错误处理和速率限制的API请求 """ @@ -256,10 +256,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): no_error_log = kwargs.pop("no_error_log", False) try: - resp = self.session.request( - method, f"{self.base_url}{endpoint}", - **kwargs - ) + resp = self.session.request(method, f"{self.base_url}{endpoint}", **kwargs) except requests.exceptions.RequestException as e: logger.error(f"【阿里云盘】{method} 请求 {endpoint} 网络错误: {str(e)}") return None @@ -278,7 +275,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): ret_data = resp.json() if ret_data.get("code"): if not no_error_log: - logger.warn(f"【阿里云盘】{method} {endpoint} 返回:{ret_data.get('code')} {ret_data.get('message')}") + logger.warn( + f"【阿里云盘】{method} {endpoint} 返回:{ret_data.get('code')} {ret_data.get('message')}" + ) if result_key: return ret_data.get(result_key) @@ -328,7 +327,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): size: 前多少字节 """ sha1 = hashlib.sha1() - with open(filepath, 'rb') as f: + with open(filepath, "rb") as f: if size: chunk = f.read(size) sha1.update(chunk) @@ -369,7 +368,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "limit": 100, "marker": next_marker, "parent_file_id": parent_file_id, - } + }, ) if resp is None: raise FileNotFoundError(f"【阿里云盘】{fileitem.path} 检索出错!") @@ -393,7 +392,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): return fileitem return None - def create_folder(self, parent_item: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: + def create_folder( + self, parent_item: schemas.FileItem, name: str + ) -> Optional[schemas.FileItem]: """ 创建目录 """ @@ -404,8 +405,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "drive_id": parent_item.drive_id, "parent_file_id": parent_item.fileid or "root", "name": name, - "type": "folder" - } + "type": "folder", + }, ) if not resp: return None @@ -422,7 +423,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): 计算文件前1KB的SHA1作为pre_hash """ sha1 = hashlib.sha1() - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: data = f.read(1024) sha1.update(data) return sha1.hexdigest() @@ -443,7 +444,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): try: tmp_int = int(hex_str, 16) except ValueError: - raise ValueError("【阿里云盘】Invalid hex string for proof code calculation") + raise ValueError( + "【阿里云盘】Invalid hex string for proof code calculation" + ) # Step 5-7: 计算读取范围 index = tmp_int % file_size @@ -453,7 +456,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): end = file_size # Step 8: 读取文件范围数据并编码 - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: f.seek(start) chunk = f.read(end - start) @@ -465,7 +468,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): 计算整个文件的SHA1作为content_hash """ sha1 = hashlib.sha1() - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: while True: chunk = f.read(8192) if not chunk: @@ -473,9 +476,15 @@ class AliPan(StorageBase, metaclass=WeakSingleton): sha1.update(chunk) return sha1.hexdigest() - def _create_file(self, drive_id: str, parent_file_id: str, - file_name: str, file_path: Path, check_name_mode="refuse", - chunk_size: int = 1 * 1024 * 1024 * 1024): + def _create_file( + self, + drive_id: str, + parent_file_id: str, + file_name: str, + file_path: Path, + check_name_mode="refuse", + chunk_size: int = 1 * 1024 * 1024 * 1024, + ): """ 创建文件请求,尝试秒传 """ @@ -495,13 +504,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "check_name_mode": check_name_mode, "size": file_size, "pre_hash": pre_hash, - "part_info_list": part_info_list + "part_info_list": part_info_list, } - resp = self._request_api( - "POST", - "/adrive/v1.0/openFile/create", - json=data - ) + resp = self._request_api("POST", "/adrive/v1.0/openFile/create", json=data) if not resp: raise Exception("【阿里云盘】创建文件失败!") if resp.get("code") == "PreHashMatched": @@ -509,24 +514,24 @@ class AliPan(StorageBase, metaclass=WeakSingleton): proof_code = self._calculate_proof_code(file_path) content_hash = self._calculate_content_hash(file_path) data.pop("pre_hash") - data.update({ - "proof_code": proof_code, - "proof_version": "v1", - "content_hash": content_hash, - "content_hash_name": "sha1", - }) - resp = self._request_api( - "POST", - "/adrive/v1.0/openFile/create", - json=data + data.update( + { + "proof_code": proof_code, + "proof_version": "v1", + "content_hash": content_hash, + "content_hash_name": "sha1", + } ) + resp = self._request_api("POST", "/adrive/v1.0/openFile/create", json=data) if not resp: raise Exception("【阿里云盘】创建文件失败!") if resp.get("code"): raise Exception(resp.get("message")) return resp - def _refresh_upload_urls(self, drive_id: str, file_id: str, upload_id: str, part_numbers: List[int]): + def _refresh_upload_urls( + self, drive_id: str, file_id: str, upload_id: str, part_numbers: List[int] + ): """ 刷新分片上传地址 """ @@ -534,18 +539,16 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "drive_id": drive_id, "file_id": file_id, "upload_id": upload_id, - "part_info_list": [{"part_number": num} for num in part_numbers] + "part_info_list": [{"part_number": num} for num in part_numbers], } resp = self._request_api( - "POST", - "/adrive/v1.0/openFile/getUploadUrl", - json=data + "POST", "/adrive/v1.0/openFile/getUploadUrl", json=data ) if not resp: raise Exception("【阿里云盘】刷新分片上传地址失败!") if resp.get("code"): raise Exception(resp.get("message")) - return resp.get('part_info_list', []) + return resp.get("part_info_list", []) @staticmethod def _upload_part(upload_url: str, data: bytes): @@ -558,15 +561,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): """ 获取已上传分片列表 """ - data = { - "drive_id": drive_id, - "file_id": file_id, - "upload_id": upload_id - } + data = {"drive_id": drive_id, "file_id": file_id, "upload_id": upload_id} resp = self._request_api( - "POST", - "/adrive/v1.0/openFile/listUploadedParts", - json=data + "POST", "/adrive/v1.0/openFile/listUploadedParts", json=data ) if not resp: raise Exception("【阿里云盘】获取已上传分片失败!") @@ -576,24 +573,20 @@ class AliPan(StorageBase, metaclass=WeakSingleton): def _complete_upload(self, drive_id: str, file_id: str, upload_id: str): """标记上传完成""" - data = { - "drive_id": drive_id, - "file_id": file_id, - "upload_id": upload_id - } - resp = self._request_api( - "POST", - "/adrive/v1.0/openFile/complete", - json=data - ) + data = {"drive_id": drive_id, "file_id": file_id, "upload_id": upload_id} + resp = self._request_api("POST", "/adrive/v1.0/openFile/complete", json=data) if not resp: raise Exception("【阿里云盘】完成上传失败!") if resp.get("code"): raise Exception(resp.get("message")) return resp - def upload(self, target_dir: schemas.FileItem, local_path: Path, - new_name: Optional[str] = None) -> Optional[schemas.FileItem]: + def upload( + self, + target_dir: schemas.FileItem, + local_path: Path, + new_name: Optional[str] = None, + ) -> Optional[schemas.FileItem]: """ 文件上传:分片、支持秒传 """ @@ -603,12 +596,14 @@ class AliPan(StorageBase, metaclass=WeakSingleton): # 1. 创建文件并检查秒传 chunk_size = 10 * 1024 * 1024 # 分片大小 10M - create_res = self._create_file(drive_id=target_dir.drive_id, - parent_file_id=target_dir.fileid, - file_name=target_name, - file_path=local_path, - chunk_size=chunk_size) - if create_res.get('rapid_upload', False): + create_res = self._create_file( + drive_id=target_dir.drive_id, + parent_file_id=target_dir.fileid, + file_name=target_name, + file_path=local_path, + chunk_size=chunk_size, + ) + if create_res.get("rapid_upload", False): logger.info(f"【阿里云盘】{target_name} 秒传完成!") return self._delay_get_item(target_path) @@ -617,33 +612,37 @@ class AliPan(StorageBase, metaclass=WeakSingleton): return self.get_item(target_path) # 2. 准备分片上传参数 - file_id = create_res.get('file_id') + file_id = create_res.get("file_id") if not file_id: logger.warn(f"【阿里云盘】创建 {target_name} 文件失败!") return None - upload_id = create_res.get('upload_id') - part_info_list = create_res.get('part_info_list') + upload_id = create_res.get("upload_id") + part_info_list = create_res.get("part_info_list") uploaded_parts = set() # 3. 获取已上传分片 - uploaded_info = self._list_uploaded_parts(drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id) - for part in uploaded_info.get('uploaded_parts', []): - uploaded_parts.add(part['part_number']) + uploaded_info = self._list_uploaded_parts( + drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id + ) + for part in uploaded_info.get("uploaded_parts", []): + uploaded_parts.add(part["part_number"]) # 4. 初始化进度条 - logger.info(f"【阿里云盘】开始上传: {local_path} -> {target_path},分片数:{len(part_info_list)}") + logger.info( + f"【阿里云盘】开始上传: {local_path} -> {target_path},分片数:{len(part_info_list)}" + ) progress_callback = transfer_process(local_path.as_posix()) # 5. 分片上传循环 uploaded_size = 0 - with open(local_path, 'rb') as f: + with open(local_path, "rb") as f: for part_info in part_info_list: if global_vars.is_transfer_stopped(local_path.as_posix()): logger.info(f"【阿里云盘】{target_name} 上传已取消!") return None # 计算分片参数 - part_num = part_info['part_number'] + part_num = part_info["part_number"] start = (part_num - 1) * chunk_size end = min(start + chunk_size, file_size) current_chunk_size = end - start @@ -664,14 +663,19 @@ class AliPan(StorageBase, metaclass=WeakSingleton): try: # 获取当前上传地址(可能刷新) if attempt > 0: - new_urls = self._refresh_upload_urls(drive_id=target_dir.drive_id, file_id=file_id, - upload_id=upload_id, part_numbers=[part_num]) - upload_url = new_urls[0]['upload_url'] + new_urls = self._refresh_upload_urls( + drive_id=target_dir.drive_id, + file_id=file_id, + upload_id=upload_id, + part_numbers=[part_num], + ) + upload_url = new_urls[0]["upload_url"] else: - upload_url = part_info['upload_url'] + upload_url = part_info["upload_url"] # 执行上传 logger.info( - f"【阿里云盘】开始 第{attempt + 1}次 上传 {target_name} 分片 {part_num} ...") + f"【阿里云盘】开始 第{attempt + 1}次 上传 {target_name} 分片 {part_num} ..." + ) response = self._upload_part(upload_url=upload_url, data=data) if response is None: continue @@ -680,9 +684,12 @@ class AliPan(StorageBase, metaclass=WeakSingleton): break else: logger.warn( - f"【阿里云盘】{target_name} 分片 {part_num} 第 {attempt + 1} 次上传失败:{response.text}!") + f"【阿里云盘】{target_name} 分片 {part_num} 第 {attempt + 1} 次上传失败:{response.text}!" + ) except Exception as e: - logger.warn(f"【阿里云盘】{target_name} 分片 {part_num} 上传异常: {str(e)}!") + logger.warn( + f"【阿里云盘】{target_name} 分片 {part_num} 上传异常: {str(e)}!" + ) # 处理上传结果 if success: @@ -690,17 +697,23 @@ class AliPan(StorageBase, metaclass=WeakSingleton): uploaded_size += current_chunk_size progress_callback((uploaded_size * 100) / file_size) else: - raise Exception(f"【阿里云盘】{target_name} 分片 {part_num} 上传失败!") + raise Exception( + f"【阿里云盘】{target_name} 分片 {part_num} 上传失败!" + ) # 6. 关闭进度条 progress_callback(100) # 7. 完成上传 - result = self._complete_upload(drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id) + result = self._complete_upload( + drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id + ) if not result: raise Exception("【阿里云盘】完成上传失败!") if result.get("code"): - logger.warn(f"【阿里云盘】{target_name} 上传失败:{result.get('message')}!") + logger.warn( + f"【阿里云盘】{target_name} 上传失败:{result.get('message')}!" + ) return self.__get_fileitem(result, parent=target_dir.path) def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]: @@ -713,7 +726,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): json={ "drive_id": fileitem.drive_id, "file_id": fileitem.fileid, - } + }, ) if not download_info: logger.error(f"【阿里云盘】获取下载链接失败: {fileitem.name}") @@ -724,7 +737,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): logger.error(f"【阿里云盘】下载链接为空: {fileitem.name}") return None - local_path = path or settings.TEMP_PATH / fileitem.name + local_path = (path or settings.TEMP_PATH) / fileitem.name # 获取文件大小 file_size = fileitem.size @@ -744,7 +757,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "Connection": "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "cross-site" + "Sec-Fetch-Site": "cross-site", } # 如果有access_token,添加到请求头 @@ -789,10 +802,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): self._request_api( "POST", "/adrive/v1.0/openFile/recyclebin/trash", - json={ - "drive_id": fileitem.drive_id, - "file_id": fileitem.fileid - } + json={"drive_id": fileitem.drive_id, "file_id": fileitem.fileid}, ) return True except requests.exceptions.HTTPError: @@ -808,8 +818,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton): json={ "drive_id": fileitem.drive_id, "file_id": fileitem.fileid, - "name": name - } + "name": name, + }, ) if not resp: return False @@ -828,9 +838,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "/adrive/v1.0/openFile/get_by_path", json={ "drive_id": drive_id or self._default_drive_id, - "file_path": path.as_posix() + "file_path": path.as_posix(), }, - no_error_log=True + no_error_log=True, ) if not resp: return None @@ -847,7 +857,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): 获取指定路径的文件夹,如不存在则创建 """ - def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + def __find_dir( + _fileitem: schemas.FileItem, _name: str + ) -> Optional[schemas.FileItem]: """ 查找下级目录中匹配名称的目录 """ @@ -863,7 +875,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton): if folder: return folder # 逐级查找和创建目录 - fileitem = schemas.FileItem(storage=self.schema.value, path="/", drive_id=self._default_drive_id) + fileitem = schemas.FileItem( + storage=self.schema.value, path="/", drive_id=self._default_drive_id + ) for part in path.parts[1:]: dir_file = __find_dir(fileitem, part) if dir_file: @@ -901,7 +915,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "file_id": fileitem.fileid, "to_drive_id": fileitem.drive_id, "to_parent_file_id": dest_fileitem.fileid, - } + }, ) if not resp: return False @@ -934,8 +948,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton): "drive_id": fileitem.drive_id, "file_id": src_fid, "to_parent_file_id": target_fileitem.fileid, - "new_name": new_name - } + "new_name": new_name, + }, ) if not resp: return False @@ -955,18 +969,14 @@ class AliPan(StorageBase, metaclass=WeakSingleton): 获取带有企业级配额信息的存储使用情况 """ try: - resp = self._request_api( - "POST", - "/adrive/v1.0/user/getSpaceInfo" - ) + resp = self._request_api("POST", "/adrive/v1.0/user/getSpaceInfo") if not resp: return None space = resp.get("personal_space_info") or {} total_size = space.get("total_size") or 0 used_size = space.get("used_size") or 0 return schemas.StorageUsage( - total=total_size, - available=total_size - used_size + total=total_size, available=total_size - used_size ) except NoCheckInException: return None diff --git a/app/modules/filemanager/storages/smb.py b/app/modules/filemanager/storages/smb.py index 55862500..163a02e0 100644 --- a/app/modules/filemanager/storages/smb.py +++ b/app/modules/filemanager/storages/smb.py @@ -5,7 +5,11 @@ from typing import List, Optional, Union import smbclient from smbclient import ClientConfig, register_session, reset_connection_cache -from smbprotocol.exceptions import SMBException, SMBResponseException, SMBAuthenticationError +from smbprotocol.exceptions import ( + SMBException, + SMBResponseException, + SMBAuthenticationError, +) from app import schemas from app.core.config import settings, global_vars @@ -22,6 +26,7 @@ class SMBConnectionError(Exception): """ SMB 连接错误 """ + pass @@ -84,7 +89,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): connection_timeout=60, port=port, auth_protocol="negotiate", # 使用协商认证 - require_secure_negotiate=False # 匿名访问时可能需要关闭安全协商 + require_secure_negotiate=False, # 匿名访问时可能需要关闭安全协商 ) # 注册会话以启用连接池 @@ -94,7 +99,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): password=self._password, port=port, encrypt=False, # 根据需要启用加密 - connection_timeout=60 + connection_timeout=60, ) # 测试连接 @@ -105,7 +110,9 @@ class SMB(StorageBase, metaclass=WeakSingleton): if self._is_anonymous_access(): logger.info(f"【SMB】匿名连接成功:{self._server_path}") else: - logger.info(f"【SMB】认证连接成功:{self._server_path} (用户:{self._username})") + logger.info( + f"【SMB】认证连接成功:{self._server_path} (用户:{self._username})" + ) except Exception as e: logger.error(f"【SMB】连接初始化失败:{e}") @@ -160,7 +167,9 @@ class SMB(StorageBase, metaclass=WeakSingleton): else: return self._server_path - def _create_fileitem(self, stat_result, file_path: str, name: str) -> schemas.FileItem: + def _create_fileitem( + self, stat_result, file_path: str, name: str + ) -> schemas.FileItem: """ 创建文件项 """ @@ -189,7 +198,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): path=relative_path, name=name, basename=name, - modify_time=modify_time + modify_time=modify_time, ) else: return schemas.FileItem( @@ -199,8 +208,8 @@ class SMB(StorageBase, metaclass=WeakSingleton): name=name, basename=Path(name).stem, extension=Path(name).suffix[1:] if Path(name).suffix else None, - size=getattr(stat_result, 'st_size', 0), - modify_time=modify_time + size=getattr(stat_result, "st_size", 0), + modify_time=modify_time, ) except Exception as e: logger.error(f"【SMB】创建文件项失败:{e}") @@ -211,7 +220,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): path=file_path.replace(self._server_path, "").replace("\\", "/"), name=name, basename=Path(name).stem, - modify_time=int(time.time()) + modify_time=int(time.time()), ) def init_storage(self): @@ -282,7 +291,9 @@ class SMB(StorageBase, metaclass=WeakSingleton): logger.error(f"【SMB】列出文件失败: {e}") return [] - def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: + def create_folder( + self, fileitem: schemas.FileItem, name: str + ) -> Optional[schemas.FileItem]: """ 创建目录 """ @@ -302,7 +313,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): path=f"{fileitem.path.rstrip('/')}/{name}/", name=name, basename=name, - modify_time=int(time.time()) + modify_time=int(time.time()), ) except Exception as e: logger.error(f"【SMB】创建目录失败: {e}") @@ -350,7 +361,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): path="/", name="", basename="", - modify_time=int(time.time()) + modify_time=int(time.time()), ) smb_path = self._normalize_path(str(path).rstrip("/")) @@ -459,8 +470,12 @@ class SMB(StorageBase, metaclass=WeakSingleton): logger.info(f"【SMB】强制删除目录成功: {smb_path}") except Exception as remove_error: # 如果还是失败,记录错误并抛出异常 - logger.error(f"【SMB】无法删除非空目录: {smb_path} - {remove_error}") - raise SMBConnectionError(f"无法删除非空目录 {smb_path}: {remove_error}") + logger.error( + f"【SMB】无法删除非空目录: {smb_path} - {remove_error}" + ) + raise SMBConnectionError( + f"无法删除非空目录 {smb_path}: {remove_error}" + ) except SMBException as e: logger.error(f"【SMB】SMB操作失败: {smb_path} - {e}") raise SMBConnectionError(f"SMB操作失败 {smb_path}: {e}") @@ -496,7 +511,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): """ 带实时进度显示的下载 """ - local_path = path or settings.TEMP_PATH / fileitem.name + local_path = (path or settings.TEMP_PATH) / fileitem.name smb_path = self._normalize_path(fileitem.path) try: self._check_connection() @@ -541,8 +556,9 @@ class SMB(StorageBase, metaclass=WeakSingleton): local_path.unlink() return None - def upload(self, fileitem: schemas.FileItem, path: Path, - new_name: Optional[str] = None) -> Optional[schemas.FileItem]: + def upload( + self, fileitem: schemas.FileItem, path: Path, new_name: Optional[str] = None + ) -> Optional[schemas.FileItem]: """ 带实时进度显示的上传 """ @@ -644,22 +660,22 @@ class SMB(StorageBase, metaclass=WeakSingleton): self._check_connection() src_path = self._normalize_path(fileitem.path) dst_path = self._normalize_path(target_file) - + # 检查源文件是否存在 if not smbclient.path.exists(src_path): raise FileNotFoundError(f"源文件不存在: {src_path}") - + # 确保目标路径的父目录存在 dst_parent = "\\".join(dst_path.rsplit("\\", 1)[:-1]) if dst_parent and not smbclient.path.exists(dst_parent): logger.info(f"【SMB】创建目标目录: {dst_parent}") smbclient.makedirs(dst_parent, exist_ok=True) - + # 尝试创建硬链接 smbclient.link(src_path, dst_path) logger.info(f"【SMB】硬链接创建成功: {src_path} -> {dst_path}") return True - + except SMBResponseException as e: # SMB协议错误,可能不支持硬链接 logger.error(f"【SMB】创建硬链接失败(当前Samba服务器可能不支持硬链接): {e}") @@ -667,8 +683,6 @@ class SMB(StorageBase, metaclass=WeakSingleton): except Exception as e: logger.error(f"【SMB】创建硬链接失败: {e}") return False - - def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool: pass @@ -682,7 +696,7 @@ class SMB(StorageBase, metaclass=WeakSingleton): volume_stat = smbclient.stat_volume(self._server_path) return schemas.StorageUsage( total=volume_stat.total_size, - available=volume_stat.caller_available_size + available=volume_stat.caller_available_size, ) except Exception as e: