mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-20 03:57:30 +08:00
Merge pull request #5504 from DDSRem-Dev/fix_smb_alipan
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user