Merge pull request #5504 from DDSRem-Dev/fix_smb_alipan

This commit is contained in:
jxxghp
2026-02-22 16:10:08 +08:00
committed by GitHub
2 changed files with 194 additions and 170 deletions

View File

@@ -38,10 +38,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
schema = StorageSchema.Alipan schema = StorageSchema.Alipan
# 支持的整理方式 # 支持的整理方式
transtype = { transtype = {"move": "移动", "copy": "复制"}
"move": "移动",
"copy": "复制"
}
# 基础url # 基础url
base_url = "https://openapi.alipan.com" base_url = "https://openapi.alipan.com"
@@ -59,9 +56,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
""" """
初始化带速率限制的会话 初始化带速率限制的会话
""" """
self.session.headers.update({ self.session.headers.update({"Content-Type": "application/json"})
"Content-Type": "application/json"
})
def _check_session(self): def _check_session(self):
""" """
@@ -76,7 +71,11 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
获取默认存储桶ID 获取默认存储桶ID
""" """
conf = self.get_conf() 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: if not drive_id:
raise NoCheckInException("【阿里云盘】请先扫码登录!") raise NoCheckInException("【阿里云盘】请先扫码登录!")
return drive_id return drive_id
@@ -94,10 +93,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
if expires_in and refresh_time + expires_in < int(time.time()): if expires_in and refresh_time + expires_in < int(time.time()):
tokens = self.__refresh_access_token(refresh_token) tokens = self.__refresh_access_token(refresh_token)
if tokens: if tokens:
self.set_config({ self.set_config({"refresh_time": int(time.time()), **tokens})
"refresh_time": int(time.time()),
**tokens
})
access_token = tokens.get("access_token") access_token = tokens.get("access_token")
if access_token: if access_token:
self.session.headers.update({"Authorization": f"Bearer {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", f"{self.base_url}/oauth/authorize/qrcode",
json={ json={
"client_id": settings.ALIPAN_APP_ID, "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": code_verifier,
"code_challenge_method": "plain" "code_challenge_method": "plain",
} },
) )
if resp is None: if resp is None:
return {}, "网络错误" return {}, "网络错误"
@@ -126,14 +127,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
if result.get("code"): if result.get("code"):
return {}, result.get("message") return {}, result.get("message")
# 持久化验证参数 # 持久化验证参数
self._auth_state = { self._auth_state = {"sid": result.get("sid"), "code_verifier": code_verifier}
"sid": result.get("sid"),
"code_verifier": code_verifier
}
# 生成二维码内容 # 生成二维码内容
return { return {"codeUrl": result.get("qrCodeUrl")}, ""
"codeUrl": result.get("qrCodeUrl")
}, ""
def check_login(self) -> Optional[Tuple[dict, str]]: def check_login(self) -> Optional[Tuple[dict, str]]:
""" """
@@ -144,7 +140,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"WaitLogin": "等待登录", "WaitLogin": "等待登录",
"ScanSuccess": "扫码成功", "ScanSuccess": "扫码成功",
"LoginSuccess": "登录成功", "LoginSuccess": "登录成功",
"QRCodeExpired": "二维码过期" "QRCodeExpired": "二维码过期",
} }
if not self._auth_state: if not self._auth_state:
@@ -163,10 +159,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
self._auth_state["authCode"] = authCode self._auth_state["authCode"] = authCode
tokens = self.__get_access_token() tokens = self.__get_access_token()
if tokens: if tokens:
self.set_config({ self.set_config({"refresh_time": int(time.time()), **tokens})
"refresh_time": int(time.time()),
**tokens
})
self.__get_drive_id() self.__get_drive_id()
return {"status": status, "tip": _status_text.get(status, "未知错误")}, "" return {"status": status, "tip": _status_text.get(status, "未知错误")}, ""
except Exception as e: except Exception as e:
@@ -184,14 +177,16 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"client_id": settings.ALIPAN_APP_ID, "client_id": settings.ALIPAN_APP_ID,
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": self._auth_state["authCode"], "code": self._auth_state["authCode"],
"code_verifier": self._auth_state["code_verifier"] "code_verifier": self._auth_state["code_verifier"],
} },
) )
if resp is None: if resp is None:
raise SessionInvalidException("【阿里云盘】获取 access_token 失败") raise SessionInvalidException("【阿里云盘】获取 access_token 失败")
result = resp.json() result = resp.json()
if result.get("code"): if result.get("code"):
raise Exception(f"【阿里云盘】{result.get('code')} - {result.get('message')}") raise Exception(
f"【阿里云盘】{result.get('code')} - {result.get('message')}"
)
return result return result
def __refresh_access_token(self, refresh_token: str) -> Optional[dict]: def __refresh_access_token(self, refresh_token: str) -> Optional[dict]:
@@ -205,30 +200,34 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
json={ json={
"client_id": settings.ALIPAN_APP_ID, "client_id": settings.ALIPAN_APP_ID,
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": refresh_token "refresh_token": refresh_token,
} },
) )
if resp is None: if resp is None:
logger.error(f"【阿里云盘】刷新 access_token 失败refresh_token={refresh_token}") logger.error(
f"【阿里云盘】刷新 access_token 失败refresh_token={refresh_token}"
)
return None return None
result = resp.json() result = resp.json()
if result.get("code"): 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 return result
def __get_drive_id(self): def __get_drive_id(self):
""" """
获取默认存储桶ID 获取默认存储桶ID
""" """
resp = self.session.post( resp = self.session.post(f"{self.base_url}/adrive/v1.0/user/getDriveInfo")
f"{self.base_url}/adrive/v1.0/user/getDriveInfo"
)
if resp is None: if resp is None:
logger.error("获取默认存储桶ID失败") logger.error("获取默认存储桶ID失败")
return None return None
result = resp.json() result = resp.json()
if result.get("code"): 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 return None
# 保存用户参数 # 保存用户参数
""" """
@@ -244,8 +243,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
self.set_config(conf) self.set_config(conf)
return None return None
def _request_api(self, method: str, endpoint: str, def _request_api(
result_key: Optional[str] = None, **kwargs) -> Optional[Union[dict, list]]: self, method: str, endpoint: str, result_key: Optional[str] = None, **kwargs
) -> Optional[Union[dict, list]]:
""" """
带错误处理和速率限制的API请求 带错误处理和速率限制的API请求
""" """
@@ -256,10 +256,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
no_error_log = kwargs.pop("no_error_log", False) no_error_log = kwargs.pop("no_error_log", False)
try: try:
resp = self.session.request( resp = self.session.request(method, f"{self.base_url}{endpoint}", **kwargs)
method, f"{self.base_url}{endpoint}",
**kwargs
)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"【阿里云盘】{method} 请求 {endpoint} 网络错误: {str(e)}") logger.error(f"【阿里云盘】{method} 请求 {endpoint} 网络错误: {str(e)}")
return None return None
@@ -278,7 +275,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
ret_data = resp.json() ret_data = resp.json()
if ret_data.get("code"): if ret_data.get("code"):
if not no_error_log: 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: if result_key:
return ret_data.get(result_key) return ret_data.get(result_key)
@@ -328,7 +327,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
size: 前多少字节 size: 前多少字节
""" """
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
if size: if size:
chunk = f.read(size) chunk = f.read(size)
sha1.update(chunk) sha1.update(chunk)
@@ -369,7 +368,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"limit": 100, "limit": 100,
"marker": next_marker, "marker": next_marker,
"parent_file_id": parent_file_id, "parent_file_id": parent_file_id,
} },
) )
if resp is None: if resp is None:
raise FileNotFoundError(f"【阿里云盘】{fileitem.path} 检索出错!") raise FileNotFoundError(f"【阿里云盘】{fileitem.path} 检索出错!")
@@ -393,7 +392,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
return fileitem return fileitem
return None 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, "drive_id": parent_item.drive_id,
"parent_file_id": parent_item.fileid or "root", "parent_file_id": parent_item.fileid or "root",
"name": name, "name": name,
"type": "folder" "type": "folder",
} },
) )
if not resp: if not resp:
return None return None
@@ -422,7 +423,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
计算文件前1KB的SHA1作为pre_hash 计算文件前1KB的SHA1作为pre_hash
""" """
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
with open(file_path, 'rb') as f: with open(file_path, "rb") as f:
data = f.read(1024) data = f.read(1024)
sha1.update(data) sha1.update(data)
return sha1.hexdigest() return sha1.hexdigest()
@@ -443,7 +444,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
try: try:
tmp_int = int(hex_str, 16) tmp_int = int(hex_str, 16)
except ValueError: except ValueError:
raise ValueError("【阿里云盘】Invalid hex string for proof code calculation") raise ValueError(
"【阿里云盘】Invalid hex string for proof code calculation"
)
# Step 5-7: 计算读取范围 # Step 5-7: 计算读取范围
index = tmp_int % file_size index = tmp_int % file_size
@@ -453,7 +456,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
end = file_size end = file_size
# Step 8: 读取文件范围数据并编码 # Step 8: 读取文件范围数据并编码
with open(file_path, 'rb') as f: with open(file_path, "rb") as f:
f.seek(start) f.seek(start)
chunk = f.read(end - start) chunk = f.read(end - start)
@@ -465,7 +468,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
计算整个文件的SHA1作为content_hash 计算整个文件的SHA1作为content_hash
""" """
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
with open(file_path, 'rb') as f: with open(file_path, "rb") as f:
while True: while True:
chunk = f.read(8192) chunk = f.read(8192)
if not chunk: if not chunk:
@@ -473,9 +476,15 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
sha1.update(chunk) sha1.update(chunk)
return sha1.hexdigest() return sha1.hexdigest()
def _create_file(self, drive_id: str, parent_file_id: str, def _create_file(
file_name: str, file_path: Path, check_name_mode="refuse", self,
chunk_size: int = 1 * 1024 * 1024 * 1024): 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, "check_name_mode": check_name_mode,
"size": file_size, "size": file_size,
"pre_hash": pre_hash, "pre_hash": pre_hash,
"part_info_list": part_info_list "part_info_list": part_info_list,
} }
resp = self._request_api( resp = self._request_api("POST", "/adrive/v1.0/openFile/create", json=data)
"POST",
"/adrive/v1.0/openFile/create",
json=data
)
if not resp: if not resp:
raise Exception("【阿里云盘】创建文件失败!") raise Exception("【阿里云盘】创建文件失败!")
if resp.get("code") == "PreHashMatched": if resp.get("code") == "PreHashMatched":
@@ -509,24 +514,24 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
proof_code = self._calculate_proof_code(file_path) proof_code = self._calculate_proof_code(file_path)
content_hash = self._calculate_content_hash(file_path) content_hash = self._calculate_content_hash(file_path)
data.pop("pre_hash") data.pop("pre_hash")
data.update({ data.update(
"proof_code": proof_code, {
"proof_version": "v1", "proof_code": proof_code,
"content_hash": content_hash, "proof_version": "v1",
"content_hash_name": "sha1", "content_hash": content_hash,
}) "content_hash_name": "sha1",
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: if not resp:
raise Exception("【阿里云盘】创建文件失败!") raise Exception("【阿里云盘】创建文件失败!")
if resp.get("code"): if resp.get("code"):
raise Exception(resp.get("message")) raise Exception(resp.get("message"))
return resp 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, "drive_id": drive_id,
"file_id": file_id, "file_id": file_id,
"upload_id": upload_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( resp = self._request_api(
"POST", "POST", "/adrive/v1.0/openFile/getUploadUrl", json=data
"/adrive/v1.0/openFile/getUploadUrl",
json=data
) )
if not resp: if not resp:
raise Exception("【阿里云盘】刷新分片上传地址失败!") raise Exception("【阿里云盘】刷新分片上传地址失败!")
if resp.get("code"): if resp.get("code"):
raise Exception(resp.get("message")) raise Exception(resp.get("message"))
return resp.get('part_info_list', []) return resp.get("part_info_list", [])
@staticmethod @staticmethod
def _upload_part(upload_url: str, data: bytes): def _upload_part(upload_url: str, data: bytes):
@@ -558,15 +561,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
""" """
获取已上传分片列表 获取已上传分片列表
""" """
data = { data = {"drive_id": drive_id, "file_id": file_id, "upload_id": upload_id}
"drive_id": drive_id,
"file_id": file_id,
"upload_id": upload_id
}
resp = self._request_api( resp = self._request_api(
"POST", "POST", "/adrive/v1.0/openFile/listUploadedParts", json=data
"/adrive/v1.0/openFile/listUploadedParts",
json=data
) )
if not resp: if not resp:
raise Exception("【阿里云盘】获取已上传分片失败!") raise Exception("【阿里云盘】获取已上传分片失败!")
@@ -576,24 +573,20 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
def _complete_upload(self, drive_id: str, file_id: str, upload_id: str): def _complete_upload(self, drive_id: str, file_id: str, upload_id: str):
"""标记上传完成""" """标记上传完成"""
data = { data = {"drive_id": drive_id, "file_id": file_id, "upload_id": upload_id}
"drive_id": drive_id, resp = self._request_api("POST", "/adrive/v1.0/openFile/complete", json=data)
"file_id": file_id,
"upload_id": upload_id
}
resp = self._request_api(
"POST",
"/adrive/v1.0/openFile/complete",
json=data
)
if not resp: if not resp:
raise Exception("【阿里云盘】完成上传失败!") raise Exception("【阿里云盘】完成上传失败!")
if resp.get("code"): if resp.get("code"):
raise Exception(resp.get("message")) raise Exception(resp.get("message"))
return resp return resp
def upload(self, target_dir: schemas.FileItem, local_path: Path, def upload(
new_name: Optional[str] = None) -> Optional[schemas.FileItem]: 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. 创建文件并检查秒传 # 1. 创建文件并检查秒传
chunk_size = 10 * 1024 * 1024 # 分片大小 10M chunk_size = 10 * 1024 * 1024 # 分片大小 10M
create_res = self._create_file(drive_id=target_dir.drive_id, create_res = self._create_file(
parent_file_id=target_dir.fileid, drive_id=target_dir.drive_id,
file_name=target_name, parent_file_id=target_dir.fileid,
file_path=local_path, file_name=target_name,
chunk_size=chunk_size) file_path=local_path,
if create_res.get('rapid_upload', False): chunk_size=chunk_size,
)
if create_res.get("rapid_upload", False):
logger.info(f"【阿里云盘】{target_name} 秒传完成!") logger.info(f"【阿里云盘】{target_name} 秒传完成!")
return self._delay_get_item(target_path) return self._delay_get_item(target_path)
@@ -617,33 +612,37 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
return self.get_item(target_path) return self.get_item(target_path)
# 2. 准备分片上传参数 # 2. 准备分片上传参数
file_id = create_res.get('file_id') file_id = create_res.get("file_id")
if not file_id: if not file_id:
logger.warn(f"【阿里云盘】创建 {target_name} 文件失败!") logger.warn(f"【阿里云盘】创建 {target_name} 文件失败!")
return None return None
upload_id = create_res.get('upload_id') upload_id = create_res.get("upload_id")
part_info_list = create_res.get('part_info_list') part_info_list = create_res.get("part_info_list")
uploaded_parts = set() uploaded_parts = set()
# 3. 获取已上传分片 # 3. 获取已上传分片
uploaded_info = self._list_uploaded_parts(drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id) uploaded_info = self._list_uploaded_parts(
for part in uploaded_info.get('uploaded_parts', []): drive_id=target_dir.drive_id, file_id=file_id, upload_id=upload_id
uploaded_parts.add(part['part_number']) )
for part in uploaded_info.get("uploaded_parts", []):
uploaded_parts.add(part["part_number"])
# 4. 初始化进度条 # 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()) progress_callback = transfer_process(local_path.as_posix())
# 5. 分片上传循环 # 5. 分片上传循环
uploaded_size = 0 uploaded_size = 0
with open(local_path, 'rb') as f: with open(local_path, "rb") as f:
for part_info in part_info_list: for part_info in part_info_list:
if global_vars.is_transfer_stopped(local_path.as_posix()): if global_vars.is_transfer_stopped(local_path.as_posix()):
logger.info(f"【阿里云盘】{target_name} 上传已取消!") logger.info(f"【阿里云盘】{target_name} 上传已取消!")
return None return None
# 计算分片参数 # 计算分片参数
part_num = part_info['part_number'] part_num = part_info["part_number"]
start = (part_num - 1) * chunk_size start = (part_num - 1) * chunk_size
end = min(start + chunk_size, file_size) end = min(start + chunk_size, file_size)
current_chunk_size = end - start current_chunk_size = end - start
@@ -664,14 +663,19 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
try: try:
# 获取当前上传地址(可能刷新) # 获取当前上传地址(可能刷新)
if attempt > 0: if attempt > 0:
new_urls = self._refresh_upload_urls(drive_id=target_dir.drive_id, file_id=file_id, new_urls = self._refresh_upload_urls(
upload_id=upload_id, part_numbers=[part_num]) drive_id=target_dir.drive_id,
upload_url = new_urls[0]['upload_url'] file_id=file_id,
upload_id=upload_id,
part_numbers=[part_num],
)
upload_url = new_urls[0]["upload_url"]
else: else:
upload_url = part_info['upload_url'] upload_url = part_info["upload_url"]
# 执行上传 # 执行上传
logger.info( 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) response = self._upload_part(upload_url=upload_url, data=data)
if response is None: if response is None:
continue continue
@@ -680,9 +684,12 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
break break
else: else:
logger.warn( logger.warn(
f"【阿里云盘】{target_name} 分片 {part_num}{attempt + 1} 次上传失败:{response.text}") f"【阿里云盘】{target_name} 分片 {part_num}{attempt + 1} 次上传失败:{response.text}"
)
except Exception as e: except Exception as e:
logger.warn(f"【阿里云盘】{target_name} 分片 {part_num} 上传异常: {str(e)}") logger.warn(
f"【阿里云盘】{target_name} 分片 {part_num} 上传异常: {str(e)}"
)
# 处理上传结果 # 处理上传结果
if success: if success:
@@ -690,17 +697,23 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
uploaded_size += current_chunk_size uploaded_size += current_chunk_size
progress_callback((uploaded_size * 100) / file_size) progress_callback((uploaded_size * 100) / file_size)
else: else:
raise Exception(f"【阿里云盘】{target_name} 分片 {part_num} 上传失败!") raise Exception(
f"【阿里云盘】{target_name} 分片 {part_num} 上传失败!"
)
# 6. 关闭进度条 # 6. 关闭进度条
progress_callback(100) progress_callback(100)
# 7. 完成上传 # 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: if not result:
raise Exception("【阿里云盘】完成上传失败!") raise Exception("【阿里云盘】完成上传失败!")
if result.get("code"): 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) return self.__get_fileitem(result, parent=target_dir.path)
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]: def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
@@ -713,7 +726,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
json={ json={
"drive_id": fileitem.drive_id, "drive_id": fileitem.drive_id,
"file_id": fileitem.fileid, "file_id": fileitem.fileid,
} },
) )
if not download_info: if not download_info:
logger.error(f"【阿里云盘】获取下载链接失败: {fileitem.name}") logger.error(f"【阿里云盘】获取下载链接失败: {fileitem.name}")
@@ -724,7 +737,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
logger.error(f"【阿里云盘】下载链接为空: {fileitem.name}") logger.error(f"【阿里云盘】下载链接为空: {fileitem.name}")
return None return None
local_path = path or settings.TEMP_PATH / fileitem.name local_path = (path or settings.TEMP_PATH) / fileitem.name
# 获取文件大小 # 获取文件大小
file_size = fileitem.size file_size = fileitem.size
@@ -744,7 +757,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"Connection": "keep-alive", "Connection": "keep-alive",
"Sec-Fetch-Dest": "empty", "Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site" "Sec-Fetch-Site": "cross-site",
} }
# 如果有access_token添加到请求头 # 如果有access_token添加到请求头
@@ -789,10 +802,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
self._request_api( self._request_api(
"POST", "POST",
"/adrive/v1.0/openFile/recyclebin/trash", "/adrive/v1.0/openFile/recyclebin/trash",
json={ json={"drive_id": fileitem.drive_id, "file_id": fileitem.fileid},
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid
}
) )
return True return True
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
@@ -808,8 +818,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
json={ json={
"drive_id": fileitem.drive_id, "drive_id": fileitem.drive_id,
"file_id": fileitem.fileid, "file_id": fileitem.fileid,
"name": name "name": name,
} },
) )
if not resp: if not resp:
return False return False
@@ -828,9 +838,9 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"/adrive/v1.0/openFile/get_by_path", "/adrive/v1.0/openFile/get_by_path",
json={ json={
"drive_id": drive_id or self._default_drive_id, "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: if not resp:
return None 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: if folder:
return 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:]: for part in path.parts[1:]:
dir_file = __find_dir(fileitem, part) dir_file = __find_dir(fileitem, part)
if dir_file: if dir_file:
@@ -901,7 +915,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"file_id": fileitem.fileid, "file_id": fileitem.fileid,
"to_drive_id": fileitem.drive_id, "to_drive_id": fileitem.drive_id,
"to_parent_file_id": dest_fileitem.fileid, "to_parent_file_id": dest_fileitem.fileid,
} },
) )
if not resp: if not resp:
return False return False
@@ -934,8 +948,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
"drive_id": fileitem.drive_id, "drive_id": fileitem.drive_id,
"file_id": src_fid, "file_id": src_fid,
"to_parent_file_id": target_fileitem.fileid, "to_parent_file_id": target_fileitem.fileid,
"new_name": new_name "new_name": new_name,
} },
) )
if not resp: if not resp:
return False return False
@@ -955,18 +969,14 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
获取带有企业级配额信息的存储使用情况 获取带有企业级配额信息的存储使用情况
""" """
try: try:
resp = self._request_api( resp = self._request_api("POST", "/adrive/v1.0/user/getSpaceInfo")
"POST",
"/adrive/v1.0/user/getSpaceInfo"
)
if not resp: if not resp:
return None return None
space = resp.get("personal_space_info") or {} space = resp.get("personal_space_info") or {}
total_size = space.get("total_size") or 0 total_size = space.get("total_size") or 0
used_size = space.get("used_size") or 0 used_size = space.get("used_size") or 0
return schemas.StorageUsage( return schemas.StorageUsage(
total=total_size, total=total_size, available=total_size - used_size
available=total_size - used_size
) )
except NoCheckInException: except NoCheckInException:
return None return None

View File

@@ -5,7 +5,11 @@ from typing import List, Optional, Union
import smbclient import smbclient
from smbclient import ClientConfig, register_session, reset_connection_cache 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 import schemas
from app.core.config import settings, global_vars from app.core.config import settings, global_vars
@@ -22,6 +26,7 @@ class SMBConnectionError(Exception):
""" """
SMB 连接错误 SMB 连接错误
""" """
pass pass
@@ -84,7 +89,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
connection_timeout=60, connection_timeout=60,
port=port, port=port,
auth_protocol="negotiate", # 使用协商认证 auth_protocol="negotiate", # 使用协商认证
require_secure_negotiate=False # 匿名访问时可能需要关闭安全协商 require_secure_negotiate=False, # 匿名访问时可能需要关闭安全协商
) )
# 注册会话以启用连接池 # 注册会话以启用连接池
@@ -94,7 +99,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
password=self._password, password=self._password,
port=port, port=port,
encrypt=False, # 根据需要启用加密 encrypt=False, # 根据需要启用加密
connection_timeout=60 connection_timeout=60,
) )
# 测试连接 # 测试连接
@@ -105,7 +110,9 @@ class SMB(StorageBase, metaclass=WeakSingleton):
if self._is_anonymous_access(): if self._is_anonymous_access():
logger.info(f"【SMB】匿名连接成功{self._server_path}") logger.info(f"【SMB】匿名连接成功{self._server_path}")
else: else:
logger.info(f"【SMB】认证连接成功{self._server_path} (用户:{self._username})") logger.info(
f"【SMB】认证连接成功{self._server_path} (用户:{self._username})"
)
except Exception as e: except Exception as e:
logger.error(f"【SMB】连接初始化失败{e}") logger.error(f"【SMB】连接初始化失败{e}")
@@ -160,7 +167,9 @@ class SMB(StorageBase, metaclass=WeakSingleton):
else: else:
return self._server_path 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, path=relative_path,
name=name, name=name,
basename=name, basename=name,
modify_time=modify_time modify_time=modify_time,
) )
else: else:
return schemas.FileItem( return schemas.FileItem(
@@ -199,8 +208,8 @@ class SMB(StorageBase, metaclass=WeakSingleton):
name=name, name=name,
basename=Path(name).stem, basename=Path(name).stem,
extension=Path(name).suffix[1:] if Path(name).suffix else None, extension=Path(name).suffix[1:] if Path(name).suffix else None,
size=getattr(stat_result, 'st_size', 0), size=getattr(stat_result, "st_size", 0),
modify_time=modify_time modify_time=modify_time,
) )
except Exception as e: except Exception as e:
logger.error(f"【SMB】创建文件项失败{e}") logger.error(f"【SMB】创建文件项失败{e}")
@@ -211,7 +220,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
path=file_path.replace(self._server_path, "").replace("\\", "/"), path=file_path.replace(self._server_path, "").replace("\\", "/"),
name=name, name=name,
basename=Path(name).stem, basename=Path(name).stem,
modify_time=int(time.time()) modify_time=int(time.time()),
) )
def init_storage(self): def init_storage(self):
@@ -282,7 +291,9 @@ class SMB(StorageBase, metaclass=WeakSingleton):
logger.error(f"【SMB】列出文件失败: {e}") logger.error(f"【SMB】列出文件失败: {e}")
return [] 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}/", path=f"{fileitem.path.rstrip('/')}/{name}/",
name=name, name=name,
basename=name, basename=name,
modify_time=int(time.time()) modify_time=int(time.time()),
) )
except Exception as e: except Exception as e:
logger.error(f"【SMB】创建目录失败: {e}") logger.error(f"【SMB】创建目录失败: {e}")
@@ -350,7 +361,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
path="/", path="/",
name="", name="",
basename="", basename="",
modify_time=int(time.time()) modify_time=int(time.time()),
) )
smb_path = self._normalize_path(str(path).rstrip("/")) smb_path = self._normalize_path(str(path).rstrip("/"))
@@ -459,8 +470,12 @@ class SMB(StorageBase, metaclass=WeakSingleton):
logger.info(f"【SMB】强制删除目录成功: {smb_path}") logger.info(f"【SMB】强制删除目录成功: {smb_path}")
except Exception as remove_error: except Exception as remove_error:
# 如果还是失败,记录错误并抛出异常 # 如果还是失败,记录错误并抛出异常
logger.error(f"【SMB】无法删除非空目录: {smb_path} - {remove_error}") logger.error(
raise SMBConnectionError(f"无法删除非空目录 {smb_path}: {remove_error}") f"【SMB】无法删除非空目录: {smb_path} - {remove_error}"
)
raise SMBConnectionError(
f"无法删除非空目录 {smb_path}: {remove_error}"
)
except SMBException as e: except SMBException as e:
logger.error(f"【SMB】SMB操作失败: {smb_path} - {e}") logger.error(f"【SMB】SMB操作失败: {smb_path} - {e}")
raise SMBConnectionError(f"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) smb_path = self._normalize_path(fileitem.path)
try: try:
self._check_connection() self._check_connection()
@@ -541,8 +556,9 @@ class SMB(StorageBase, metaclass=WeakSingleton):
local_path.unlink() local_path.unlink()
return None return None
def upload(self, fileitem: schemas.FileItem, path: Path, def upload(
new_name: Optional[str] = None) -> Optional[schemas.FileItem]: 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() self._check_connection()
src_path = self._normalize_path(fileitem.path) src_path = self._normalize_path(fileitem.path)
dst_path = self._normalize_path(target_file) dst_path = self._normalize_path(target_file)
# 检查源文件是否存在 # 检查源文件是否存在
if not smbclient.path.exists(src_path): if not smbclient.path.exists(src_path):
raise FileNotFoundError(f"源文件不存在: {src_path}") raise FileNotFoundError(f"源文件不存在: {src_path}")
# 确保目标路径的父目录存在 # 确保目标路径的父目录存在
dst_parent = "\\".join(dst_path.rsplit("\\", 1)[:-1]) dst_parent = "\\".join(dst_path.rsplit("\\", 1)[:-1])
if dst_parent and not smbclient.path.exists(dst_parent): if dst_parent and not smbclient.path.exists(dst_parent):
logger.info(f"【SMB】创建目标目录: {dst_parent}") logger.info(f"【SMB】创建目标目录: {dst_parent}")
smbclient.makedirs(dst_parent, exist_ok=True) smbclient.makedirs(dst_parent, exist_ok=True)
# 尝试创建硬链接 # 尝试创建硬链接
smbclient.link(src_path, dst_path) smbclient.link(src_path, dst_path)
logger.info(f"【SMB】硬链接创建成功: {src_path} -> {dst_path}") logger.info(f"【SMB】硬链接创建成功: {src_path} -> {dst_path}")
return True return True
except SMBResponseException as e: except SMBResponseException as e:
# SMB协议错误可能不支持硬链接 # SMB协议错误可能不支持硬链接
logger.error(f"【SMB】创建硬链接失败(当前Samba服务器可能不支持硬链接): {e}") logger.error(f"【SMB】创建硬链接失败(当前Samba服务器可能不支持硬链接): {e}")
@@ -667,8 +683,6 @@ class SMB(StorageBase, metaclass=WeakSingleton):
except Exception as e: except Exception as e:
logger.error(f"【SMB】创建硬链接失败: {e}") logger.error(f"【SMB】创建硬链接失败: {e}")
return False return False
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool: def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
@@ -682,7 +696,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
volume_stat = smbclient.stat_volume(self._server_path) volume_stat = smbclient.stat_volume(self._server_path)
return schemas.StorageUsage( return schemas.StorageUsage(
total=volume_stat.total_size, total=volume_stat.total_size,
available=volume_stat.caller_available_size available=volume_stat.caller_available_size,
) )
except Exception as e: except Exception as e: