2025-10-29 22:20:21 +08:00
|
|
|
|
import mimetypes
|
|
|
|
|
|
import os
|
|
|
|
|
|
from concurrent.futures import as_completed, ThreadPoolExecutor
|
|
|
|
|
|
from typing import Any, Dict
|
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
from loguru import logger
|
|
|
|
|
|
from PyQt6.QtCore import QThread, pyqtSignal
|
|
|
|
|
|
|
|
|
|
|
|
from app.core import cfg, qconfig
|
|
|
|
|
|
from app.core import miaoStarsBasicApi
|
|
|
|
|
|
from app.core import policyConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ListFileThread(QThread):
|
|
|
|
|
|
"""个人仓内文件搜索工作线程"""
|
|
|
|
|
|
|
|
|
|
|
|
listDictSignal = pyqtSignal(object)
|
|
|
|
|
|
errorSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
path,
|
|
|
|
|
|
):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.path = path
|
|
|
|
|
|
logger.debug(f"初始化文件加载线程,path: {path}")
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""执行API请求"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"开始API请求")
|
|
|
|
|
|
response = miaoStarsBasicApi.list(self.path)
|
|
|
|
|
|
if response["code"] == 0:
|
|
|
|
|
|
self.listDictSignal.emit(response)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"API请求失败, 错误: {response['msg']}")
|
|
|
|
|
|
self.errorSignal.emit(response["msg"])
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception(f"API请求异常, 异常信息: {e}")
|
|
|
|
|
|
self.errorSignal.emit(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ListSearchThread(QThread):
|
|
|
|
|
|
"""搜索线程"""
|
|
|
|
|
|
|
|
|
|
|
|
listDictSignal = pyqtSignal(object)
|
|
|
|
|
|
errorSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, searchContent, searchType):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.searchContent = searchContent
|
|
|
|
|
|
self.searchType = searchType
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"初始化搜索工作线程, 搜索内容: {searchContent}, 搜索类型: {searchType}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""执行API请求"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"开始API请求")
|
|
|
|
|
|
response = miaoStarsBasicApi.wareSearch(self.searchContent, self.searchType)
|
|
|
|
|
|
|
|
|
|
|
|
if response["code"] == 0:
|
|
|
|
|
|
logger.success(f"API请求成功")
|
|
|
|
|
|
self.listDictSignal.emit(response)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"API请求失败, 错误: {response['msg']}")
|
|
|
|
|
|
self.errorSignal.emit(response["msg"])
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception(f"API请求异常, 异常信息: {str(e)}")
|
|
|
|
|
|
self.errorSignal.emit(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ListShareThread(QThread):
|
|
|
|
|
|
"""分享搜索工作线程"""
|
|
|
|
|
|
|
|
|
|
|
|
listDictSignal = pyqtSignal(object)
|
|
|
|
|
|
errorSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, keyword, orderBy, order, page):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.keyword = keyword
|
|
|
|
|
|
self.orderBy = orderBy
|
|
|
|
|
|
self.order = order
|
|
|
|
|
|
self.page = page
|
|
|
|
|
|
logger.debug(f"初始化API工作线程")
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""执行API请求"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# shareSearch方法只接受4个参数,pageSize参数会在shareSearch方法内部通过params字典设置
|
|
|
|
|
|
response = miaoStarsBasicApi.shareSearch(
|
|
|
|
|
|
self.keyword, self.orderBy, self.order, self.page
|
|
|
|
|
|
)
|
|
|
|
|
|
if response["code"] == 0:
|
|
|
|
|
|
self.listDictSignal.emit(response)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"API请求失败, 错误: {response['msg']}")
|
|
|
|
|
|
self.errorSignal.emit(response["msg"])
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception(f"API请求异常, 异常信息: {str(e)}")
|
|
|
|
|
|
self.errorSignal.emit(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateFolderThread(QThread):
|
|
|
|
|
|
"""创建文件夹的线程"""
|
|
|
|
|
|
|
|
|
|
|
|
successSignal = pyqtSignal()
|
|
|
|
|
|
errorSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, folderName):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.folderName = folderName
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""执行文件夹创建操作"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
response = miaoStarsBasicApi.createFolder(self.folderName)
|
|
|
|
|
|
|
|
|
|
|
|
if response["code"] == 0:
|
|
|
|
|
|
self.successSignal.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"创建文件夹失败, 错误: {response['msg']}")
|
|
|
|
|
|
self.errorSignal.emit(response["msg"])
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception(f"创建文件夹异常, 异常信息: {e}")
|
|
|
|
|
|
self.errorSignal.emit(f"网络请求失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeleteFileThread(QThread):
|
|
|
|
|
|
"""删除文件线程"""
|
|
|
|
|
|
|
|
|
|
|
|
successDelete = pyqtSignal()
|
|
|
|
|
|
errorDelete = pyqtSignal(str)
|
|
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
def __init__(self, fileUri: str, fileType: str):
|
2025-10-29 22:20:21 +08:00
|
|
|
|
super().__init__()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.debug(f"初始化删除文件线程 - URI: {fileUri}, 类型: {fileType}")
|
|
|
|
|
|
self.fileUri = fileUri
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.fileType = fileType
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info(f"开始删除文件 - URI: {self.fileUri}, 类型: {self.fileType}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
try:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
response = miaoStarsBasicApi.deleteFile(self.fileUri, self.fileType)
|
2025-10-29 22:20:21 +08:00
|
|
|
|
if response["code"] == 0:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info(f"文件删除成功 - URI: {self.fileUri}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.successDelete.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(
|
2025-11-01 20:14:35 +08:00
|
|
|
|
f"文件删除失败 - URI: {self.fileUri}, 错误: {response.get('msg', '未知错误')}"
|
2025-10-29 22:20:21 +08:00
|
|
|
|
)
|
|
|
|
|
|
self.errorDelete.emit(f"删除失败: {response.get('msg', '未知错误')}")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"文件删除过程中发生异常 - ID: {self.fileId}: {e}")
|
|
|
|
|
|
self.errorDelete.emit("系统错误,请稍后重试")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GetShareFileInfoThread(QThread):
|
|
|
|
|
|
"""获取分享文件信息线程"""
|
|
|
|
|
|
|
|
|
|
|
|
shareFileInfoSignal = pyqtSignal(object)
|
|
|
|
|
|
errorSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, shareId: str):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.shareId = shareId
|
|
|
|
|
|
logger.debug(f"初始化获取分享文件信息线程 - ID: {shareId}")
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""执行API请求"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = miaoStarsBasicApi.getShareFileInfo(self.shareId)
|
|
|
|
|
|
if response["code"] == 0:
|
|
|
|
|
|
self.shareFileInfoSignal.emit(response)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"API请求失败, 错误: {response['msg']}")
|
|
|
|
|
|
self.errorSignal.emit(response["msg"])
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception(f"API请求异常, 异常信息: {str(e)}")
|
|
|
|
|
|
self.errorSignal.emit(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateFileContentThread(QThread):
|
|
|
|
|
|
successUpdated = pyqtSignal()
|
|
|
|
|
|
errorUpdated = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, fileId: str, content: str):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.content = content
|
|
|
|
|
|
self.fileId = fileId
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 设置超时时间,例如10秒连接,30秒读取
|
|
|
|
|
|
response = miaoStarsBasicApi.updateFileContent(self.fileId, self.content)
|
|
|
|
|
|
if response.get("code") == 0:
|
|
|
|
|
|
self.successUpdated.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.errorUpdated.emit(response.get("msg"))
|
|
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
|
|
self.errorUpdated.emit("请求超时,请检查网络连接")
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
self.errorUpdated.emit(f"网络请求错误: {str(e)}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.errorUpdated.emit(f"保存时发生错误: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UploadThread(QThread):
|
|
|
|
|
|
# 定义信号用于通信
|
|
|
|
|
|
uploadApplicationApprovedSignal = pyqtSignal() # 上传成功信号(文件名)
|
|
|
|
|
|
uploadFinished = pyqtSignal() # 上传完成信号(成功)
|
|
|
|
|
|
uploadFailed = pyqtSignal(str) # 上传失败信号(错误信息)
|
|
|
|
|
|
uploadProgress = pyqtSignal(float, int, int) # 进度信号:百分比, 已上传大小, 总大小
|
|
|
|
|
|
uploadCancelled = pyqtSignal() # 上传取消信号
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_path: str):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
self.file_name = os.path.basename(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加取消标志和文件对象引用
|
|
|
|
|
|
self._is_cancelled = False
|
|
|
|
|
|
self._file_obj = None
|
|
|
|
|
|
|
|
|
|
|
|
# 配置信息
|
|
|
|
|
|
# 设置Cloudreve V4的上传应用端点
|
|
|
|
|
|
self.applicationUrl = "/file/upload"
|
|
|
|
|
|
|
|
|
|
|
|
self.current_path = policyConfig.returnCurrentPath()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 获取存储策略,如果为None则使用默认值PqI8
|
|
|
|
|
|
self.policy = policyConfig.returnPolicy().get("id", "PqI8")
|
|
|
|
|
|
if self.policy is None:
|
|
|
|
|
|
self.policy = "PqI8"
|
|
|
|
|
|
logger.info("存储策略ID为None,已设置默认值PqI8")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
self.headers = {
|
|
|
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
|
|
|
|
"Accept": "application/json, text/plain, */*",
|
|
|
|
|
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
|
|
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
# 删除硬编码的Origin和Referer,使用miaoStarsBasicApi中已配置的请求头
|
|
|
|
|
|
"Connection": "keep-alive",
|
|
|
|
|
|
"Sec-Fetch-Dest": "empty",
|
|
|
|
|
|
"Sec-Fetch-Mode": "cors",
|
|
|
|
|
|
"Sec-Fetch-Site": "same-origin",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def getMimeType(self, file_path):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取文件的 MIME 类型
|
|
|
|
|
|
:param file_path: 文件路径
|
|
|
|
|
|
:return: MIME 类型
|
|
|
|
|
|
"""
|
|
|
|
|
|
mime_type, _ = mimetypes.guess_type(file_path)
|
|
|
|
|
|
return mime_type if mime_type else "application/octet-stream"
|
|
|
|
|
|
|
|
|
|
|
|
def _prepareUploadData(self) -> Dict[str, Any]:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
"""准备上传数据,符合Cloudreve V4 API规范"""
|
2025-10-29 22:20:21 +08:00
|
|
|
|
try:
|
|
|
|
|
|
modification_time = os.path.getmtime(self.file_path)
|
|
|
|
|
|
size = os.path.getsize(self.file_path)
|
2025-11-01 20:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 构建符合API要求的URI格式: cloudreve://my{path}/{name}
|
|
|
|
|
|
# 处理路径,确保不会有重复斜杠和前缀
|
|
|
|
|
|
# 清理路径,确保不包含cloudreve://my前缀
|
|
|
|
|
|
clean_current_path = self.current_path.replace("cloudreve://my", "")
|
|
|
|
|
|
# 处理路径,确保不会有重复斜杠
|
|
|
|
|
|
path_part = clean_current_path if clean_current_path.startswith('/') else f'/{clean_current_path}'
|
|
|
|
|
|
uri = f"cloudreve://my{path_part}/{self.file_name}"
|
|
|
|
|
|
|
|
|
|
|
|
# 确保路径格式正确,移除重复的前缀
|
|
|
|
|
|
uri = uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
|
|
|
|
|
|
|
|
|
|
|
# 更健壮地处理重复文件名的情况
|
|
|
|
|
|
# 分割路径并去重
|
|
|
|
|
|
path_parts = uri.split('/')
|
|
|
|
|
|
if len(path_parts) > 1:
|
|
|
|
|
|
# 检查最后一个部分是否是文件名
|
|
|
|
|
|
if path_parts[-1] == self.file_name:
|
|
|
|
|
|
# 检查倒数第二个部分是否也是文件名
|
|
|
|
|
|
if len(path_parts) > 2 and path_parts[-2] == self.file_name:
|
|
|
|
|
|
# 移除重复的文件名部分
|
|
|
|
|
|
path_parts.pop(-2)
|
|
|
|
|
|
uri = '/'.join(path_parts)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"构建上传URI: {uri}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2025-11-01 20:14:35 +08:00
|
|
|
|
"uri": uri,
|
2025-10-29 22:20:21 +08:00
|
|
|
|
"size": size,
|
|
|
|
|
|
"policy_id": self.policy,
|
|
|
|
|
|
"last_modified": int(modification_time * 1000), # 转换为毫秒
|
|
|
|
|
|
"mime_type": self.getMimeType(self.file_path),
|
|
|
|
|
|
}
|
|
|
|
|
|
except (OSError, ValueError) as e:
|
|
|
|
|
|
logger.error(f"准备请求失败: {e}")
|
2025-11-01 20:14:35 +08:00
|
|
|
|
raise
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
def _uploadWithProgress(self, upload_url: str, credential: str, total_size: int):
|
|
|
|
|
|
"""带进度显示的上传方法"""
|
|
|
|
|
|
try:
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("_uploadWithProgress方法开始执行")
|
|
|
|
|
|
logger.info(f"参数检查 - URL: {upload_url}, Credential是否存在: {credential is not None}")
|
|
|
|
|
|
|
|
|
|
|
|
# 处理credential可能为None的情况
|
|
|
|
|
|
if credential is None:
|
|
|
|
|
|
logger.warning("Credential为None,尝试不使用Authorization头进行上传")
|
|
|
|
|
|
# 对于本地存储策略,可能不需要credential
|
|
|
|
|
|
auth_header = {}
|
|
|
|
|
|
else:
|
|
|
|
|
|
auth_header = {"Authorization": credential}
|
|
|
|
|
|
|
2025-10-29 22:20:21 +08:00
|
|
|
|
# 打开文件
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info(f"开始打开文件: {self.file_path}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self._file_obj = open(self.file_path, "rb")
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("文件打开成功")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 准备上传头信息
|
|
|
|
|
|
upload_headers = {
|
|
|
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
|
|
|
|
"Accept": "*/*",
|
|
|
|
|
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
|
|
|
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
|
|
|
|
"Content-Type": self.getMimeType(self.file_path), # 使用正确的MIME类型
|
2025-11-01 20:14:35 +08:00
|
|
|
|
"Content-Length": str(total_size), # 添加必需的Content-Length头
|
2025-10-29 22:20:21 +08:00
|
|
|
|
# 删除硬编码的Origin和Referer,使用miaoStarsBasicApi中已配置的请求头
|
|
|
|
|
|
"Sec-Fetch-Dest": "empty",
|
|
|
|
|
|
"Sec-Fetch-Mode": "cors",
|
|
|
|
|
|
"Sec-Fetch-Site": "cross-site",
|
2025-11-01 20:14:35 +08:00
|
|
|
|
**auth_header, # 只在credential不为None时添加Authorization头
|
2025-10-29 22:20:21 +08:00
|
|
|
|
}
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info(f"上传头信息准备完成: {upload_headers}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 尝试发送预检请求,但不阻塞主流程
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info("尝试发送OPTIONS预检请求")
|
|
|
|
|
|
options_result = miaoStarsBasicApi.returnSession().options(
|
|
|
|
|
|
upload_url, headers=upload_headers, timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
options_result.raise_for_status()
|
|
|
|
|
|
logger.info("OPTIONS预检请求完成")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"OPTIONS预检请求失败,但继续上传: {str(e)}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 创建自定义请求体以支持进度监控和取消
|
|
|
|
|
|
class ProgressFileObject:
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self, file_obj, total_size, progress_callback, cancel_check
|
|
|
|
|
|
):
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("创建ProgressFileObject对象")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
self.file_obj = file_obj
|
|
|
|
|
|
self.total_size = total_size
|
|
|
|
|
|
self.progress_callback = progress_callback
|
|
|
|
|
|
self.cancel_check = cancel_check
|
|
|
|
|
|
self.uploaded = 0
|
|
|
|
|
|
|
|
|
|
|
|
def read(self, size=-1):
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self.cancel_check():
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("检测到取消上传请求")
|
|
|
|
|
|
raise InterruptedError("Upload cancelled by user")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
data = self.file_obj.read(size)
|
|
|
|
|
|
if data:
|
|
|
|
|
|
self.uploaded += len(data)
|
|
|
|
|
|
progress = (
|
|
|
|
|
|
(self.uploaded / self.total_size) * 100
|
|
|
|
|
|
if self.total_size > 0
|
|
|
|
|
|
else 0
|
|
|
|
|
|
)
|
|
|
|
|
|
self.progress_callback(progress, self.uploaded, self.total_size)
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.debug(f"上传进度: {progress:.2f}% ({self.uploaded}/{self.total_size} 字节)")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
|
return self.total_size
|
|
|
|
|
|
|
|
|
|
|
|
# 创建带进度监控的文件对象
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("创建进度监控文件对象")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
progress_file = ProgressFileObject(
|
|
|
|
|
|
self._file_obj,
|
|
|
|
|
|
total_size,
|
|
|
|
|
|
lambda progress, uploaded, total: self.uploadProgress.emit(
|
|
|
|
|
|
progress, uploaded, total
|
|
|
|
|
|
),
|
|
|
|
|
|
lambda: self._is_cancelled,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 执行实际上传
|
|
|
|
|
|
logger.info(f"开始上传文件,总大小: {total_size} 字节")
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info(f"上传URL: {upload_url}")
|
|
|
|
|
|
logger.info(f"上传头信息(已脱敏): {{\n 'User-Agent': '...', \n 'Content-Type': '{upload_headers.get("Content-Type")}',\n 'Content-Length': '{upload_headers.get("Content-Length")}',\n 'Authorization': '***' if 'Authorization' in upload_headers else 'None'\n }}")
|
|
|
|
|
|
|
|
|
|
|
|
# 修复:优化上传逻辑,先尝试PUT方法(Cloudreve V4本地存储通常使用PUT)
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 首先尝试使用PUT方法
|
|
|
|
|
|
logger.info("尝试使用PUT方法上传")
|
|
|
|
|
|
upload_result = miaoStarsBasicApi.returnSession().put(
|
|
|
|
|
|
upload_url,
|
|
|
|
|
|
data=progress_file,
|
|
|
|
|
|
headers=upload_headers,
|
|
|
|
|
|
timeout=120,
|
|
|
|
|
|
stream=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"PUT方法上传完成,响应状态: {upload_result.status_code}")
|
|
|
|
|
|
upload_result.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
return upload_result
|
|
|
|
|
|
except InterruptedError:
|
|
|
|
|
|
# 用户取消上传
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"PUT方法上传失败: {str(e)}")
|
|
|
|
|
|
# 尝试使用POST方法
|
|
|
|
|
|
logger.info("尝试使用POST方法重新上传...")
|
|
|
|
|
|
# 重新打开文件
|
|
|
|
|
|
if self._file_obj:
|
|
|
|
|
|
self._file_obj.close()
|
|
|
|
|
|
self._file_obj = open(self.file_path, "rb")
|
|
|
|
|
|
|
|
|
|
|
|
# 重新创建进度文件对象
|
|
|
|
|
|
progress_file = ProgressFileObject(
|
|
|
|
|
|
self._file_obj,
|
|
|
|
|
|
total_size,
|
|
|
|
|
|
lambda progress, uploaded, total: self.uploadProgress.emit(
|
|
|
|
|
|
progress, uploaded, total
|
|
|
|
|
|
),
|
|
|
|
|
|
lambda: self._is_cancelled,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用POST方法上传
|
|
|
|
|
|
upload_result = miaoStarsBasicApi.returnSession().post(
|
|
|
|
|
|
upload_url,
|
|
|
|
|
|
data=progress_file,
|
|
|
|
|
|
headers=upload_headers,
|
|
|
|
|
|
timeout=120,
|
|
|
|
|
|
stream=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"POST方法上传完成,响应状态: {upload_result.status_code}")
|
|
|
|
|
|
upload_result.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
return upload_result
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"上传过程中发生错误: {e}")
|
|
|
|
|
|
self.uploadFailed.emit(f"上传过程中发生错误: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# 确保文件被关闭
|
|
|
|
|
|
if self._file_obj:
|
|
|
|
|
|
self._file_obj.close()
|
|
|
|
|
|
self._file_obj = None
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""主上传逻辑"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("上传在开始前已被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"开始上传文件: {self.file_name}")
|
|
|
|
|
|
|
|
|
|
|
|
# 准备上传数据
|
|
|
|
|
|
upload_data = self._prepareUploadData()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查policy_id是否为None,如果是则尝试获取默认策略
|
|
|
|
|
|
if upload_data.get('policy_id') is None:
|
|
|
|
|
|
logger.warning("存储策略ID为None,尝试获取默认存储策略")
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取用户的存储策略列表
|
|
|
|
|
|
policies_response = miaoStarsBasicApi.getPolicy()
|
|
|
|
|
|
if policies_response.get('code') == 0 and policies_response.get('data'):
|
|
|
|
|
|
# 使用第一个可用的策略
|
|
|
|
|
|
default_policy = policies_response['data'][0]
|
|
|
|
|
|
upload_data['policy_id'] = default_policy.get('id')
|
|
|
|
|
|
logger.info(f"已设置默认存储策略: {default_policy.get('name')} (ID: {upload_data['policy_id']})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("无法获取存储策略列表,使用硬编码默认策略PqI8")
|
|
|
|
|
|
upload_data['policy_id'] = 'PqI8'
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取存储策略失败: {str(e)},使用硬编码默认策略PqI8")
|
|
|
|
|
|
upload_data['policy_id'] = 'PqI8'
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("上传在获取上传URL前被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 执行上传请求获取上传URL
|
|
|
|
|
|
logger.info("请求上传URL")
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 直接使用basicApi构建完整URL,保留/api/v4后缀
|
|
|
|
|
|
# Cloudreve V4 API要求上传接口路径为/api/v4/file/upload
|
|
|
|
|
|
full_url = f"{miaoStarsBasicApi.basicApi}{self.applicationUrl}"
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"构建的上传请求URL: {full_url}")
|
|
|
|
|
|
logger.info(f"上传请求数据: {upload_data}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().put(
|
|
|
|
|
|
full_url, json=upload_data, headers=self.headers, timeout=30
|
|
|
|
|
|
)
|
2025-11-01 20:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 记录响应状态码和内容
|
|
|
|
|
|
logger.info(f"上传URL请求响应状态码: {response.status_code}")
|
|
|
|
|
|
response_text = response.text
|
|
|
|
|
|
logger.info(f"上传URL请求响应内容长度: {len(response_text)} 字符")
|
|
|
|
|
|
|
|
|
|
|
|
# 如果响应内容较长,只记录前100个字符
|
|
|
|
|
|
if len(response_text) > 100:
|
|
|
|
|
|
logger.info(f"上传URL请求响应内容预览: {response_text[:100]}...")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"上传URL请求响应内容: {response_text}")
|
|
|
|
|
|
|
2025-10-29 22:20:21 +08:00
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 添加错误处理,检查响应是否为有效的JSON
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
logger.info(f"JSON解析成功,响应code: {result.get('code', '未找到code字段')}")
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
logger.error(f"JSON解析失败: {e}")
|
|
|
|
|
|
logger.error(f"无法解析的响应内容: {response_text}")
|
|
|
|
|
|
# 抛出异常让上层处理
|
|
|
|
|
|
raise ValueError(f"无效的响应格式: {response_text}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
if result.get("code") == 0:
|
|
|
|
|
|
self.uploadApplicationApprovedSignal.emit()
|
2025-11-01 20:14:35 +08:00
|
|
|
|
data = result.get("data", {})
|
|
|
|
|
|
upload_urls = data.get("upload_urls") # 使用正确的字段名,符合API规范
|
|
|
|
|
|
credential = data.get("credential")
|
|
|
|
|
|
session_id = data.get("session_id")
|
|
|
|
|
|
storage_policy = data.get("storage_policy", {})
|
|
|
|
|
|
policy_type = storage_policy.get("type", "")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"获取到上传URL数量: {len(upload_urls) if upload_urls else 0}")
|
|
|
|
|
|
logger.debug(f"存储策略类型: {policy_type}, Session ID: {session_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# 优先检查是否有session_id,因为这是必须的
|
|
|
|
|
|
if not session_id:
|
|
|
|
|
|
logger.error("未获取到session_id,无法构建上传URL")
|
|
|
|
|
|
self.uploadFailed.emit("未获取到session_id,无法构建上传URL")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 根据存储策略类型和API响应构建正确的上传URL
|
|
|
|
|
|
if upload_urls and len(upload_urls) > 0:
|
|
|
|
|
|
# 对于远程存储策略,使用返回的upload_urls,并正确设置chunk参数
|
|
|
|
|
|
# 注意:根据URL情况决定是否添加?或&
|
|
|
|
|
|
if "?" in upload_urls[0]:
|
|
|
|
|
|
self.uploadUrl = upload_urls[0] + "&chunk=0"
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.uploadUrl = upload_urls[0] + "?chunk=0"
|
|
|
|
|
|
logger.info(f"使用远程存储上传URL: {self.uploadUrl}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 对于本地存储策略或没有提供upload_urls的情况,使用session_id构建上传URL
|
|
|
|
|
|
# 正确的API路径格式: /file/upload/{sessionId}/{index}
|
|
|
|
|
|
# 修复:分块索引应该从0开始,符合Cloudreve V4 API规范
|
|
|
|
|
|
self.uploadUrl = f"{miaoStarsBasicApi.basicApi}/file/upload/{session_id}/0"
|
|
|
|
|
|
logger.info(f"使用本地存储分块上传路径: {self.uploadUrl}")
|
|
|
|
|
|
logger.info(f"获取到上传URL: {self.uploadUrl}")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取文件总大小
|
|
|
|
|
|
total_size = os.path.getsize(self.file_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 执行带进度显示的上传
|
2025-11-01 20:14:35 +08:00
|
|
|
|
logger.info("开始调用_uploadWithProgress方法执行上传")
|
|
|
|
|
|
try:
|
|
|
|
|
|
upload_result = self._uploadWithProgress(
|
|
|
|
|
|
self.uploadUrl, credential, total_size
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("_uploadWithProgress方法调用完成")
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.uploadCancelled.emit()
|
|
|
|
|
|
return
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
2025-11-01 20:14:35 +08:00
|
|
|
|
# 处理上传结果
|
|
|
|
|
|
logger.info(f"上传响应状态码: {upload_result.status_code}")
|
|
|
|
|
|
# 限制日志长度,避免日志过大
|
|
|
|
|
|
response_text = upload_result.text
|
|
|
|
|
|
if len(response_text) > 100:
|
|
|
|
|
|
logger.info(f"上传响应内容预览: {response_text[:100]}...")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"上传响应内容: {response_text}")
|
|
|
|
|
|
|
|
|
|
|
|
# 确保上传成功
|
|
|
|
|
|
upload_result.raise_for_status()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"上传过程中发生异常: {str(e)}")
|
|
|
|
|
|
logger.error(f"异常类型: {type(e).__name__}")
|
|
|
|
|
|
self.uploadFailed.emit(f"上传过程中发生错误: {str(e)}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 解析上传响应
|
|
|
|
|
|
upload_response = upload_result.json()
|
|
|
|
|
|
logger.info(f"上传响应JSON解析成功: {upload_response}")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查上传是否成功完成
|
|
|
|
|
|
if upload_response.get('code') == 0:
|
|
|
|
|
|
logger.info("文件上传成功完成")
|
|
|
|
|
|
self.uploadFinished.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = upload_response.get('msg', '上传失败')
|
|
|
|
|
|
logger.error(f"上传失败: {error_msg}")
|
|
|
|
|
|
self.uploadFailed.emit(error_msg)
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
# 如果响应不是有效的JSON,只要状态码成功,也视为上传成功
|
|
|
|
|
|
logger.warning(f"上传响应不是有效的JSON: {e}")
|
|
|
|
|
|
logger.info("基于成功状态码,假设上传成功")
|
|
|
|
|
|
self.uploadFinished.emit()
|
2025-10-29 22:20:21 +08:00
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = result.get("msg", "上传失败")
|
|
|
|
|
|
self.uploadFailed.emit(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
except FileNotFoundError as e:
|
|
|
|
|
|
error_msg = f"未发现文件: {e}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
self.uploadFailed.emit(error_msg)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.uploadCancelled.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = f"上传失败: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
self.uploadFailed.emit(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
def cancelUpload(self):
|
|
|
|
|
|
"""取消上传操作"""
|
|
|
|
|
|
logger.info("取消上传请求")
|
|
|
|
|
|
self._is_cancelled = True
|
|
|
|
|
|
# 关闭文件对象(如果存在)
|
|
|
|
|
|
if self._file_obj:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._file_obj.close()
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
finally:
|
|
|
|
|
|
self._file_obj = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadThread(QThread):
|
|
|
|
|
|
# 定义信号用于通信
|
|
|
|
|
|
downloadUrlAcquired = pyqtSignal(str) # 下载URL获取成功信号
|
|
|
|
|
|
downloadFinished = pyqtSignal() # 下载完成信号
|
|
|
|
|
|
downloadFailed = pyqtSignal(str) # 下载失败信号
|
|
|
|
|
|
downloadProgress = pyqtSignal(float, int, int) # 进度信号:百分比, 已下载大小, 总大小
|
|
|
|
|
|
downloadCancelled = pyqtSignal() # 下载取消信号
|
|
|
|
|
|
fileSavePathSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_id: str, file_path: str = None):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.file_id = file_id
|
|
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
self.save_path = qconfig.get(cfg.downloadSavePath)
|
|
|
|
|
|
self.chunk_size = 1024 * 1024 # 分块大小,默认1MB
|
|
|
|
|
|
|
|
|
|
|
|
# 添加取消标志和文件对象引用
|
|
|
|
|
|
self._is_cancelled = False
|
|
|
|
|
|
self._file_obj = None
|
|
|
|
|
|
self.download_url = None
|
|
|
|
|
|
self.total_size = 0
|
|
|
|
|
|
self.downloaded_size = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 配置信息
|
|
|
|
|
|
# 使用Cloudreve V4 API正确的下载URL创建端点
|
|
|
|
|
|
self.download_application_url = "/file/url"
|
|
|
|
|
|
|
|
|
|
|
|
self.headers = {
|
|
|
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
|
|
|
|
"Accept": "application/json, text/plain, */*",
|
|
|
|
|
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
|
|
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
# 删除硬编码的Origin和Referer,使用miaoStarsBasicApi中已配置的请求头
|
|
|
|
|
|
"Connection": "keep-alive",
|
|
|
|
|
|
"Sec-Fetch-Dest": "empty",
|
|
|
|
|
|
"Sec-Fetch-Mode": "cors",
|
|
|
|
|
|
"Sec-Fetch-Site": "same-origin",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileType(self, url):
|
|
|
|
|
|
"""
|
|
|
|
|
|
通过HTTP请求获取文件类型
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 发送HEAD请求获取文件信息(不下载内容)
|
|
|
|
|
|
response = requests.head(url, timeout=10, allow_redirects=True)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
# 从Content-Type头部获取文件类型
|
|
|
|
|
|
content_type = response.headers.get("Content-Type", "")
|
|
|
|
|
|
|
|
|
|
|
|
# 常见文件类型映射
|
|
|
|
|
|
type_mapping = {
|
|
|
|
|
|
"text/plain": "文本文件",
|
|
|
|
|
|
"text/python": "Python文件",
|
|
|
|
|
|
"application/x-python-code": "Python文件",
|
|
|
|
|
|
"application/octet-stream": "二进制文件",
|
|
|
|
|
|
"application/pdf": "PDF文件",
|
|
|
|
|
|
"image/jpeg": "JPEG图片",
|
|
|
|
|
|
"image/png": "PNG图片",
|
|
|
|
|
|
"application/zip": "ZIP压缩文件",
|
|
|
|
|
|
"text/html": "HTML文件",
|
|
|
|
|
|
"application/json": "JSON文件",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
file_type = type_mapping.get(content_type, content_type)
|
|
|
|
|
|
file_size = response.headers.get("Content-Length", "未知")
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"content_type": content_type,
|
|
|
|
|
|
"file_type": file_type,
|
|
|
|
|
|
"file_size": file_size,
|
|
|
|
|
|
"filename": urlparse(url).path.split("/")[-1],
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {"error": f"请求失败,状态码: {response.status_code}"}
|
|
|
|
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
return {"error": f"请求错误: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
|
|
def _getDownloadUrl(self):
|
|
|
|
|
|
"""获取下载URL"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"===== 开始下载文件API请求 =====")
|
|
|
|
|
|
logger.info(f"[传入内容] 请求下载URL,文件路径: {self.file_path}")
|
|
|
|
|
|
logger.info(f"[传入内容] 文件ID: {self.file_id}")
|
|
|
|
|
|
logger.info(f"[传入内容] 保存路径: {self.save_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# 准备Cloudreve V4 API所需的请求体
|
|
|
|
|
|
import urllib.parse
|
|
|
|
|
|
|
|
|
|
|
|
# 确保file_path不为None
|
|
|
|
|
|
if not self.file_path:
|
|
|
|
|
|
raise Exception("文件路径为空,无法构建下载URI")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[传入内容] 验证通过,开始构建API请求")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用接收到的file_path构建正确的URI格式
|
|
|
|
|
|
# 对于Cloudreve V4 API,需要使用正确的cloudreve://格式
|
|
|
|
|
|
# 先尝试解码可能已经编码过的部分,避免双重编码
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 先解码已有的URL编码部分
|
|
|
|
|
|
decoded_path = urllib.parse.unquote(self.file_path)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查并移除可能重复的文件名部分
|
|
|
|
|
|
path_parts = decoded_path.split('/')
|
|
|
|
|
|
if len(path_parts) > 1:
|
|
|
|
|
|
# 检查最后两个部分是否相同
|
|
|
|
|
|
if len(path_parts) >= 3 and path_parts[-1] == path_parts[-2]:
|
|
|
|
|
|
# 移除重复的文件名部分
|
|
|
|
|
|
path_parts.pop(-1)
|
|
|
|
|
|
decoded_path = '/'.join(path_parts)
|
|
|
|
|
|
logger.info(f"[传入内容] 已移除重复的文件名部分: {decoded_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# 然后对解码后的路径进行一次正确的编码
|
|
|
|
|
|
uri = urllib.parse.quote(decoded_path, safe=':/')
|
|
|
|
|
|
logger.info(f"[传入内容] 处理后的URI: {uri}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"路径处理失败,使用原始路径: {str(e)}")
|
|
|
|
|
|
# 如果处理失败,则使用原始路径(可能已经是正确格式)
|
|
|
|
|
|
uri = self.file_path
|
|
|
|
|
|
|
|
|
|
|
|
# 准备请求体 - 按照API要求使用uris数组包含完整的Cloudreve URI
|
|
|
|
|
|
request_body = {
|
|
|
|
|
|
"uris": [uri],
|
|
|
|
|
|
"download": True
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[传入内容] 构建的请求URI: {uri}")
|
|
|
|
|
|
logger.info(f"[传入内容] 完整API请求体: {request_body}")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建完整的API请求URL
|
|
|
|
|
|
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4')
|
|
|
|
|
|
full_url = f"{base_url}/api/v4/file/url"
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[传入内容] API请求URL: {full_url}")
|
|
|
|
|
|
logger.info(f"[传入内容] 请求头部信息: {self.headers}")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[传入内容] 发送POST请求到API...")
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().post(
|
|
|
|
|
|
full_url,
|
|
|
|
|
|
json=request_body,
|
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
|
timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[返回内容] API响应状态码: {response.status_code}")
|
|
|
|
|
|
logger.info(f"[返回内容] API响应头部: {dict(response.headers)}")
|
|
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
logger.info(f"===== [返回内容] API响应内容开始 =====")
|
|
|
|
|
|
logger.info(f"[返回内容] 响应JSON: {result}")
|
|
|
|
|
|
logger.info(f"[返回内容] 响应代码: {result.get('code')}")
|
|
|
|
|
|
logger.info(f"[返回内容] 响应消息: {result.get('msg')}")
|
|
|
|
|
|
if 'data' in result:
|
|
|
|
|
|
logger.info(f"[返回内容] 响应数据部分: {result['data']}")
|
|
|
|
|
|
if 'urls' in result['data']:
|
|
|
|
|
|
logger.info(f"[返回内容] URLs数组长度: {len(result['data']['urls'])}")
|
|
|
|
|
|
for i, url_info in enumerate(result['data']['urls']):
|
|
|
|
|
|
logger.info(f"[返回内容] URL {i+1}: {url_info.get('url')}")
|
|
|
|
|
|
if 'aggregated_error' in result['data']:
|
|
|
|
|
|
logger.info(f"[返回内容] 聚合错误信息: {result['data']['aggregated_error']}")
|
|
|
|
|
|
logger.info(f"===== [返回内容] API响应内容结束 =====")
|
|
|
|
|
|
|
|
|
|
|
|
if result.get("code") == 0:
|
|
|
|
|
|
# 验证响应数据结构
|
|
|
|
|
|
data = result.get("data")
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
raise Exception("响应中缺少data字段")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查URLs数组
|
|
|
|
|
|
urls = data.get("urls", [])
|
|
|
|
|
|
if not urls or not isinstance(urls, list) or len(urls) == 0:
|
|
|
|
|
|
raise Exception("响应中没有URL信息或URLs数组为空")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取第一个URL并验证
|
|
|
|
|
|
download_url_info = urls[0]
|
|
|
|
|
|
if not isinstance(download_url_info, dict) or "url" not in download_url_info:
|
|
|
|
|
|
raise Exception("URL信息格式不正确")
|
|
|
|
|
|
|
|
|
|
|
|
download_url = download_url_info.get("url")
|
|
|
|
|
|
if not download_url or download_url.strip() == "":
|
|
|
|
|
|
# URL为空的情况下,尝试检查是否有aggregated_error提供更多信息
|
|
|
|
|
|
error_details = []
|
|
|
|
|
|
if "aggregated_error" in result:
|
|
|
|
|
|
aggregated_errors = result["aggregated_error"]
|
|
|
|
|
|
for uri, error_info in aggregated_errors.items():
|
|
|
|
|
|
error_details.append(f"URI: {uri},错误: {error_info.get('msg', '未知错误')}")
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试使用备用URI格式 - 直接使用文件ID作为content路径
|
|
|
|
|
|
if not error_details:
|
|
|
|
|
|
error_details.append("尝试使用备用URI格式...")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建备用请求体,直接使用API路径而不是URI
|
|
|
|
|
|
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4')
|
|
|
|
|
|
# 使用self.file_id而不是未定义的file_id变量
|
|
|
|
|
|
alternate_url = f"{base_url}/api/v4/file/content/{self.file_id}/0"
|
|
|
|
|
|
logger.info(f"[返回内容] 尝试备用下载URL: {alternate_url}")
|
|
|
|
|
|
|
|
|
|
|
|
# 发送HEAD请求检查备用URL是否可用
|
|
|
|
|
|
try:
|
|
|
|
|
|
head_response = miaoStarsBasicApi.returnSession().head(
|
|
|
|
|
|
alternate_url,
|
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
|
timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
if head_response.status_code == 200:
|
|
|
|
|
|
download_url = alternate_url
|
|
|
|
|
|
logger.info(f"[返回内容] 备用URL可用,使用: {download_url}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_details.append(f"备用URL检查失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
if not download_url or download_url.strip() == "":
|
|
|
|
|
|
error_msg = f"获取下载URL失败: 下载URL为空。可能的原因: {'; '.join(error_details)}"
|
|
|
|
|
|
logger.error(f"[返回内容] 错误详情: {error_details}")
|
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
# 处理下载URL
|
|
|
|
|
|
logger.info(f"[返回内容] 获取到下载URL: {download_url}")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查URL是否为相对路径,如果是则拼接完整URL
|
|
|
|
|
|
if download_url.startswith('/'):
|
|
|
|
|
|
self.download_url = base_url + download_url
|
|
|
|
|
|
logger.info(f"[返回内容] 拼接完整下载URL: {self.download_url}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.download_url = download_url
|
|
|
|
|
|
logger.info(f"[返回内容] 完整下载URL: {self.download_url}")
|
|
|
|
|
|
|
|
|
|
|
|
self.downloadUrlAcquired.emit(self.download_url)
|
|
|
|
|
|
logger.info("[返回内容] 成功获取下载URL")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取文件信息
|
|
|
|
|
|
logger.info("[返回内容] 获取文件信息")
|
|
|
|
|
|
self.fileBasicInfo = self._getFileType(download_url)
|
|
|
|
|
|
logger.info(f"[返回内容] 文件信息: {self.fileBasicInfo}")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新保存路径
|
|
|
|
|
|
if "filename" in self.fileBasicInfo:
|
|
|
|
|
|
self.save_path = f"{self.save_path}/{self.fileBasicInfo['filename']}"
|
|
|
|
|
|
self.fileSavePathSignal.emit(self.save_path)
|
|
|
|
|
|
logger.info(f"[返回内容] 文件保存路径已更新: {self.save_path}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("[返回内容] 文件信息中缺少文件名")
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 处理错误响应
|
|
|
|
|
|
error_msg = result.get("msg", "获取下载URL失败")
|
|
|
|
|
|
if "aggregated_error" in result:
|
|
|
|
|
|
aggregated_errors = result["aggregated_error"]
|
|
|
|
|
|
for uri, error_info in aggregated_errors.items():
|
|
|
|
|
|
error_msg += f",URI: {uri},错误: {error_info.get('msg', '未知错误')}"
|
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
logger.info(f"===== API请求异常 =====")
|
|
|
|
|
|
logger.error(f"HTTP请求异常: {str(e)}")
|
|
|
|
|
|
if hasattr(e, 'response') and e.response:
|
|
|
|
|
|
logger.error(f"异常响应状态码: {e.response.status_code}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.error(f"异常响应内容: {e.response.json()}")
|
|
|
|
|
|
except:
|
|
|
|
|
|
logger.error(f"异常响应文本: {e.response.text}")
|
|
|
|
|
|
logger.info(f"===== API请求异常结束 =====")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.info(f"===== 下载URL获取失败 =====")
|
|
|
|
|
|
logger.error(f"获取下载URL失败: {str(e)}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
logger.error(f"错误堆栈: {traceback.format_exc()}")
|
|
|
|
|
|
logger.info(f"===== 下载URL获取失败结束 =====")
|
|
|
|
|
|
self.downloadFailed.emit(f"获取下载URL失败: {str(e)}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSize(self, url):
|
|
|
|
|
|
"""获取文件总大小 - 完整解决方案"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 方法1: 尝试HEAD请求
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().head(url, timeout=10)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
size = response.headers.get("content-length")
|
|
|
|
|
|
if size and size != "0":
|
|
|
|
|
|
size = int(size)
|
|
|
|
|
|
logger.info(f"通过HEAD请求获取文件大小: {size} 字节")
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
# 方法2: 尝试Range请求
|
|
|
|
|
|
logger.info("HEAD请求未返回文件大小,尝试Range请求...")
|
|
|
|
|
|
range_size = self._getFileSizeWithRange(url)
|
|
|
|
|
|
if range_size > 0:
|
|
|
|
|
|
return range_size
|
|
|
|
|
|
|
|
|
|
|
|
# 方法3: 最后尝试GET请求估算
|
|
|
|
|
|
logger.info("Range请求失败,尝试GET请求估算...")
|
|
|
|
|
|
get_size = self._getFileSizeByGet(url)
|
|
|
|
|
|
return get_size
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSizeWithRange(self, url):
|
|
|
|
|
|
"""使用Range请求获取文件大小"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
headers = {"Range": "bytes=0-0"}
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
url, headers=headers, timeout=10, stream=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 206:
|
|
|
|
|
|
content_range = response.headers.get("content-range")
|
|
|
|
|
|
if content_range:
|
|
|
|
|
|
total_size = content_range.split("/")[-1]
|
|
|
|
|
|
if total_size.isdigit():
|
|
|
|
|
|
size = int(total_size)
|
|
|
|
|
|
logger.info(f"通过Range请求获取文件大小: {size} 字节")
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Range请求获取文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSizeByGet(self, url):
|
|
|
|
|
|
"""通过GET请求估算文件大小"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
url, stream=True, timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
size = 0
|
|
|
|
|
|
max_read_size = 131072 # 最多读取128KB来估算
|
|
|
|
|
|
|
|
|
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
|
|
|
size += len(chunk)
|
|
|
|
|
|
if size >= max_read_size:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果读取到数据,记录估算值
|
|
|
|
|
|
if size > 0:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"通过部分下载估算文件大小: {size} 字节(注意:这只是一个估算值)"
|
|
|
|
|
|
)
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"GET请求估算文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _downloadChunk(self, chunk_info):
|
|
|
|
|
|
"""下载单个分块"""
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
start, end, chunk_num = chunk_info
|
|
|
|
|
|
headers = self.headers.copy()
|
|
|
|
|
|
headers["Range"] = f"bytes={start}-{end}"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
self.download_url, headers=headers, stream=True, timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
chunk_data = b""
|
|
|
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if chunk:
|
|
|
|
|
|
chunk_data += chunk
|
|
|
|
|
|
# 更新进度
|
|
|
|
|
|
self.downloaded_size += len(chunk)
|
|
|
|
|
|
progress = (
|
|
|
|
|
|
(self.downloaded_size / self.total_size) * 100
|
|
|
|
|
|
if self.total_size > 0
|
|
|
|
|
|
else 0
|
|
|
|
|
|
)
|
|
|
|
|
|
self.downloadProgress.emit(
|
|
|
|
|
|
progress, self.downloaded_size, self.total_size
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return chunk_num, chunk_data
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"下载分块 {chunk_num} 失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _downloadWithProgress(self):
|
|
|
|
|
|
"""带进度显示的多线程下载方法"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取文件总大小
|
|
|
|
|
|
self.total_size = self._getFileSize(self.download_url)
|
|
|
|
|
|
# if self.total_size == 0:
|
|
|
|
|
|
# raise Exception("无法获取文件大小")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建保存目录
|
|
|
|
|
|
os.makedirs(os.path.dirname(self.save_path), exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算分块信息
|
|
|
|
|
|
chunks = []
|
|
|
|
|
|
num_chunks = (self.total_size + self.chunk_size - 1) // self.chunk_size
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(num_chunks):
|
|
|
|
|
|
start = i * self.chunk_size
|
|
|
|
|
|
end = min((i + 1) * self.chunk_size - 1, self.total_size - 1)
|
|
|
|
|
|
chunks.append((start, end, i))
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"开始多线程下载,总大小: {self.total_size} 字节,分块数: {num_chunks}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用线程池进行多线程下载
|
|
|
|
|
|
chunks_data = [None] * num_chunks
|
|
|
|
|
|
successful_chunks = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 根据文件大小决定线程数
|
|
|
|
|
|
max_workers = min(8, max(2, num_chunks // 4))
|
|
|
|
|
|
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
|
|
|
# 提交所有下载任务
|
|
|
|
|
|
future_to_chunk = {
|
|
|
|
|
|
executor.submit(self._downloadChunk, chunk): chunk
|
|
|
|
|
|
for chunk in chunks
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 收集完成的任务
|
|
|
|
|
|
for future in as_completed(future_to_chunk):
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
executor.shutdown(wait=False)
|
|
|
|
|
|
raise Exception("下载被取消")
|
|
|
|
|
|
|
|
|
|
|
|
result = future.result()
|
|
|
|
|
|
if result:
|
|
|
|
|
|
chunk_num, chunk_data = result
|
|
|
|
|
|
chunks_data[chunk_num] = chunk_data
|
|
|
|
|
|
successful_chunks += 1
|
|
|
|
|
|
# logger.debug(f"分块 {chunk_num} 下载完成")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否所有分块都下载成功
|
|
|
|
|
|
if successful_chunks != num_chunks:
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
f"下载不完整,成功分块: {successful_chunks}/{num_chunks}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 合并所有分块并写入文件
|
|
|
|
|
|
logger.info("开始合并分块数据")
|
|
|
|
|
|
with open(self.save_path, "wb") as f:
|
|
|
|
|
|
for chunk_data in chunks_data:
|
|
|
|
|
|
if chunk_data:
|
|
|
|
|
|
f.write(chunk_data)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"文件下载完成: {self.save_path}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"下载过程中发生错误: {e}")
|
|
|
|
|
|
# 清理可能已创建的不完整文件
|
|
|
|
|
|
if os.path.exists(self.save_path):
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(self.save_path)
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""主下载逻辑"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("下载在开始前已被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"开始下载文件,ID: {self.file_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取下载URL
|
|
|
|
|
|
if not self._getDownloadUrl():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("下载在获取URL后被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 执行带进度显示的多线程下载
|
|
|
|
|
|
self._downloadWithProgress()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.downloadCancelled.emit()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 下载成功
|
|
|
|
|
|
self.downloadFinished.emit()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.downloadCancelled.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = f"下载失败: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
self.downloadFailed.emit(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
def cancelDownload(self):
|
|
|
|
|
|
"""取消下载操作"""
|
|
|
|
|
|
logger.info("取消下载请求")
|
|
|
|
|
|
self._is_cancelled = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadShareThread(QThread):
|
|
|
|
|
|
# 定义信号用于通信
|
|
|
|
|
|
downloadUrlAcquired = pyqtSignal(str) # 下载URL获取成功信号
|
|
|
|
|
|
downloadFinished = pyqtSignal() # 下载完成信号
|
|
|
|
|
|
downloadFailed = pyqtSignal(str) # 下载失败信号
|
|
|
|
|
|
downloadProgress = pyqtSignal(float, int, int) # 进度信号:百分比, 已下载大小, 总大小
|
|
|
|
|
|
downloadCancelled = pyqtSignal() # 下载取消信号
|
|
|
|
|
|
fileSavePathSignal = pyqtSignal(str)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_id: str, file_path: str):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.file_id = file_id.split(".")[-1]
|
|
|
|
|
|
print(file_id.split("."))
|
|
|
|
|
|
self.save_path = qconfig.get(cfg.downloadSavePath)
|
|
|
|
|
|
self.chunk_size = 1024 * 1024 # 分块大小,默认1MB
|
|
|
|
|
|
|
|
|
|
|
|
# 添加取消标志和文件对象引用
|
|
|
|
|
|
self._is_cancelled = False
|
|
|
|
|
|
self._file_obj = None
|
|
|
|
|
|
self.download_url = None
|
|
|
|
|
|
self.total_size = 0
|
|
|
|
|
|
self.path = file_id.split(".")[0] + "." + file_id.split(".")[1]
|
|
|
|
|
|
self.downloaded_size = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 配置信息
|
|
|
|
|
|
# 使用V4 API下载分享文件
|
|
|
|
|
|
self.download_application_url = f"/share/download/{self.file_id}?path={self.path}"
|
|
|
|
|
|
print(self.download_application_url)
|
|
|
|
|
|
|
|
|
|
|
|
self.headers = {
|
|
|
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
|
|
|
|
"Accept": "application/json, text/plain, */*",
|
|
|
|
|
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
|
|
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
# 删除硬编码的Origin和Referer,使用miaoStarsBasicApi中已配置的请求头
|
|
|
|
|
|
"Connection": "keep-alive",
|
|
|
|
|
|
"Sec-Fetch-Dest": "empty",
|
|
|
|
|
|
"Sec-Fetch-Mode": "cors",
|
|
|
|
|
|
"Sec-Fetch-Site": "same-origin",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileType(self, url):
|
|
|
|
|
|
"""
|
|
|
|
|
|
通过HTTP请求获取文件类型
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 发送HEAD请求获取文件信息(不下载内容)
|
|
|
|
|
|
response = requests.head(url, timeout=30, allow_redirects=True)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
# 从Content-Type头部获取文件类型
|
|
|
|
|
|
content_type = response.headers.get("Content-Type", "")
|
|
|
|
|
|
|
|
|
|
|
|
# 常见文件类型映射
|
|
|
|
|
|
type_mapping = {
|
|
|
|
|
|
"text/plain": "文本文件",
|
|
|
|
|
|
"text/python": "Python文件",
|
|
|
|
|
|
"application/x-python-code": "Python文件",
|
|
|
|
|
|
"application/octet-stream": "二进制文件",
|
|
|
|
|
|
"application/pdf": "PDF文件",
|
|
|
|
|
|
"image/jpeg": "JPEG图片",
|
|
|
|
|
|
"image/png": "PNG图片",
|
|
|
|
|
|
"application/zip": "ZIP压缩文件",
|
|
|
|
|
|
"text/html": "HTML文件",
|
|
|
|
|
|
"application/json": "JSON文件",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
file_type = type_mapping.get(content_type, content_type)
|
|
|
|
|
|
file_size = response.headers.get("Content-Length", "未知")
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"content_type": content_type,
|
|
|
|
|
|
"file_type": file_type,
|
|
|
|
|
|
"file_size": file_size,
|
|
|
|
|
|
"filename": urlparse(url).path.split("/")[-1],
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {"error": f"请求失败,状态码: {response.status_code}"}
|
|
|
|
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
return {"error": f"请求错误: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
|
|
def _getDownloadUrl(self):
|
|
|
|
|
|
"""获取分享文件下载URL"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"请求分享文件下载URL,文件ID: {self.file_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# 准备Cloudreve V4 API所需的请求体
|
|
|
|
|
|
# 按照API要求构建正确的URI格式
|
|
|
|
|
|
import urllib.parse
|
|
|
|
|
|
|
|
|
|
|
|
# 确保路径部分被正确URL编码
|
|
|
|
|
|
path = self.path if self.path else ''
|
|
|
|
|
|
if path:
|
|
|
|
|
|
# 只对路径部分进行编码,保留/分隔符
|
|
|
|
|
|
parts = path.split('/')
|
|
|
|
|
|
encoded_parts = [urllib.parse.quote(part) for part in parts if part]
|
|
|
|
|
|
encoded_path = '/' + '/'.join(encoded_parts)
|
|
|
|
|
|
else:
|
|
|
|
|
|
encoded_path = ''
|
|
|
|
|
|
|
|
|
|
|
|
# 构建完整的Cloudreve URI - 使用文件ID构建有效的URI
|
|
|
|
|
|
file_uri = f"cloudreve://share/{self.file_id}{encoded_path}"
|
|
|
|
|
|
|
|
|
|
|
|
# 准备请求体 - 按照API要求使用uris数组包含完整的Cloudreve URI
|
|
|
|
|
|
request_body = {
|
|
|
|
|
|
"uris": [file_uri],
|
|
|
|
|
|
"download": True,
|
|
|
|
|
|
"skip_error": True,
|
|
|
|
|
|
"use_primary_site_url": True
|
|
|
|
|
|
# 移除no_cache参数,使用API默认缓存行为
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"使用正确格式的分享文件URI: {file_uri}")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建完整的API请求URL
|
|
|
|
|
|
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4')
|
|
|
|
|
|
full_url = f"{base_url}/api/v4/file/url"
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"请求URL: {full_url}")
|
|
|
|
|
|
logger.debug(f"请求体: {request_body}")
|
|
|
|
|
|
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().post(
|
|
|
|
|
|
full_url,
|
|
|
|
|
|
json=request_body,
|
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
|
timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
logger.debug(f"分享文件下载URL响应: {result}")
|
|
|
|
|
|
|
|
|
|
|
|
if result.get("code") == 0:
|
|
|
|
|
|
# 验证响应数据结构
|
|
|
|
|
|
data = result.get("data")
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
raise Exception("响应中缺少data字段")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查URLs数组
|
|
|
|
|
|
urls = data.get("urls", [])
|
|
|
|
|
|
if not urls or not isinstance(urls, list) or len(urls) == 0:
|
|
|
|
|
|
raise Exception("响应中没有URL信息或URLs数组为空")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取第一个URL并验证
|
|
|
|
|
|
download_url_info = urls[0]
|
|
|
|
|
|
if not isinstance(download_url_info, dict) or "url" not in download_url_info:
|
|
|
|
|
|
raise Exception("URL信息格式不正确")
|
|
|
|
|
|
|
|
|
|
|
|
download_url = download_url_info.get("url")
|
|
|
|
|
|
if not download_url or download_url.strip() == "":
|
|
|
|
|
|
# URL为空的情况下,尝试检查是否有aggregated_error提供更多信息
|
|
|
|
|
|
error_details = []
|
|
|
|
|
|
if "aggregated_error" in result:
|
|
|
|
|
|
aggregated_errors = result["aggregated_error"]
|
|
|
|
|
|
for uri, error_info in aggregated_errors.items():
|
|
|
|
|
|
error_details.append(f"URI: {uri},错误: {error_info.get('msg', '未知错误')}")
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试使用备用URI格式 - 直接使用文件ID作为share路径
|
|
|
|
|
|
if not error_details:
|
|
|
|
|
|
error_details.append("尝试使用备用URI格式...")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建备用请求体,直接使用API路径而不是URI
|
|
|
|
|
|
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4')
|
|
|
|
|
|
alternate_url = f"{base_url}/api/v4/share/{file_id}/download"
|
|
|
|
|
|
if path and path.startswith('/'):
|
|
|
|
|
|
alternate_url += path
|
|
|
|
|
|
logger.debug(f"尝试分享文件备用下载URL: {alternate_url}")
|
|
|
|
|
|
|
|
|
|
|
|
# 发送HEAD请求检查备用URL是否可用
|
|
|
|
|
|
try:
|
|
|
|
|
|
head_response = miaoStarsBasicApi.returnSession().head(
|
|
|
|
|
|
alternate_url,
|
|
|
|
|
|
headers=self.headers,
|
|
|
|
|
|
timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
if head_response.status_code == 200:
|
|
|
|
|
|
download_url = alternate_url
|
|
|
|
|
|
logger.info(f"备用URL可用,使用: {download_url}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_details.append(f"备用URL检查失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
if not download_url or download_url.strip() == "":
|
|
|
|
|
|
error_msg = f"获取分享文件下载URL失败: 下载URL为空。可能的原因: {'; '.join(error_details)}"
|
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
# 处理下载URL
|
|
|
|
|
|
logger.info(f"获取到分享文件下载URL: {download_url}")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查URL是否为相对路径,如果是则拼接完整URL
|
|
|
|
|
|
if download_url.startswith('/'):
|
|
|
|
|
|
self.download_url = base_url + download_url
|
|
|
|
|
|
logger.info(f"拼接完整下载URL: {self.download_url}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.download_url = download_url
|
|
|
|
|
|
|
|
|
|
|
|
self.downloadUrlAcquired.emit(self.download_url)
|
|
|
|
|
|
logger.info("成功获取分享文件下载URL")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取文件信息
|
|
|
|
|
|
logger.info("获取文件信息")
|
|
|
|
|
|
self.fileBasicInfo = self._getFileType(download_url)
|
|
|
|
|
|
logger.debug(f"文件信息: {self.fileBasicInfo}")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新保存路径
|
|
|
|
|
|
if "filename" in self.fileBasicInfo:
|
|
|
|
|
|
self.save_path = f"{self.save_path}/{self.fileBasicInfo['filename']}"
|
|
|
|
|
|
self.fileSavePathSignal.emit(self.save_path)
|
|
|
|
|
|
logger.info(f"文件保存路径已更新: {self.save_path}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("文件信息中缺少文件名")
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 处理错误响应
|
|
|
|
|
|
error_msg = result.get("msg", "获取下载URL失败")
|
|
|
|
|
|
if "aggregated_error" in result:
|
|
|
|
|
|
aggregated_errors = result["aggregated_error"]
|
|
|
|
|
|
for uri, error_info in aggregated_errors.items():
|
|
|
|
|
|
error_msg += f",URI: {uri},错误: {error_info.get('msg', '未知错误')}"
|
|
|
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取分享文件下载URL失败: {e}")
|
|
|
|
|
|
self.downloadFailed.emit(f"获取分享文件下载URL失败: {str(e)}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSize(self, url):
|
|
|
|
|
|
"""获取文件总大小 - 完整解决方案"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 方法1: 尝试HEAD请求
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().head(url, timeout=10)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
size = response.headers.get("content-length")
|
|
|
|
|
|
if size and size != "0":
|
|
|
|
|
|
size = int(size)
|
|
|
|
|
|
logger.info(f"通过HEAD请求获取文件大小: {size} 字节")
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
# 方法2: 尝试Range请求
|
|
|
|
|
|
logger.info("HEAD请求未返回文件大小,尝试Range请求...")
|
|
|
|
|
|
range_size = self._getFileSizeWithRange(url)
|
|
|
|
|
|
if range_size > 0:
|
|
|
|
|
|
return range_size
|
|
|
|
|
|
|
|
|
|
|
|
# 方法3: 最后尝试GET请求估算
|
|
|
|
|
|
logger.info("Range请求失败,尝试GET请求估算...")
|
|
|
|
|
|
get_size = self._getFileSizeByGet(url)
|
|
|
|
|
|
return get_size
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSizeWithRange(self, url):
|
|
|
|
|
|
"""使用Range请求获取文件大小"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
headers = {"Range": "bytes=0-0"}
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
url, headers=headers, timeout=10, stream=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 206:
|
|
|
|
|
|
content_range = response.headers.get("content-range")
|
|
|
|
|
|
if content_range:
|
|
|
|
|
|
total_size = content_range.split("/")[-1]
|
|
|
|
|
|
if total_size.isdigit():
|
|
|
|
|
|
size = int(total_size)
|
|
|
|
|
|
logger.info(f"通过Range请求获取文件大小: {size} 字节")
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Range请求获取文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _getFileSizeByGet(self, url):
|
|
|
|
|
|
"""通过GET请求估算文件大小"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
url, stream=True, timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
size = 0
|
|
|
|
|
|
max_read_size = 131072 # 最多读取128KB来估算
|
|
|
|
|
|
|
|
|
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
|
|
|
size += len(chunk)
|
|
|
|
|
|
if size >= max_read_size:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果读取到数据,记录估算值
|
|
|
|
|
|
if size > 0:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"通过部分下载估算文件大小: {size} 字节(注意:这只是一个估算值)"
|
|
|
|
|
|
)
|
|
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
response.close()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"GET请求估算文件大小失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _downloadChunk(self, chunk_info):
|
|
|
|
|
|
"""下载单个分块"""
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
start, end, chunk_num = chunk_info
|
|
|
|
|
|
headers = self.headers.copy()
|
|
|
|
|
|
headers["Range"] = f"bytes={start}-{end}"
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = miaoStarsBasicApi.returnSession().get(
|
|
|
|
|
|
self.download_url, headers=headers, stream=True, timeout=30
|
|
|
|
|
|
)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
chunk_data = b""
|
|
|
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if chunk:
|
|
|
|
|
|
chunk_data += chunk
|
|
|
|
|
|
# 更新进度
|
|
|
|
|
|
self.downloaded_size += len(chunk)
|
|
|
|
|
|
progress = (
|
|
|
|
|
|
(self.downloaded_size / self.total_size) * 100
|
|
|
|
|
|
if self.total_size > 0
|
|
|
|
|
|
else 0
|
|
|
|
|
|
)
|
|
|
|
|
|
self.downloadProgress.emit(
|
|
|
|
|
|
progress, self.downloaded_size, self.total_size
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return chunk_num, chunk_data
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"下载分块 {chunk_num} 失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _downloadWithProgress(self):
|
|
|
|
|
|
"""带进度显示的多线程下载方法"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取文件总大小
|
|
|
|
|
|
self.total_size = self._getFileSize(self.download_url)
|
|
|
|
|
|
# if self.total_size == 0:
|
|
|
|
|
|
# raise Exception("无法获取文件大小")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建保存目录
|
|
|
|
|
|
os.makedirs(os.path.dirname(self.save_path), exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算分块信息
|
|
|
|
|
|
chunks = []
|
|
|
|
|
|
num_chunks = (self.total_size + self.chunk_size - 1) // self.chunk_size
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(num_chunks):
|
|
|
|
|
|
start = i * self.chunk_size
|
|
|
|
|
|
end = min((i + 1) * self.chunk_size - 1, self.total_size - 1)
|
|
|
|
|
|
chunks.append((start, end, i))
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"开始多线程下载,总大小: {self.total_size} 字节,分块数: {num_chunks}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用线程池进行多线程下载
|
|
|
|
|
|
chunks_data = [None] * num_chunks
|
|
|
|
|
|
successful_chunks = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 根据文件大小决定线程数
|
|
|
|
|
|
max_workers = min(8, max(2, num_chunks // 4))
|
|
|
|
|
|
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
|
|
|
# 提交所有下载任务
|
|
|
|
|
|
future_to_chunk = {
|
|
|
|
|
|
executor.submit(self._downloadChunk, chunk): chunk
|
|
|
|
|
|
for chunk in chunks
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 收集完成的任务
|
|
|
|
|
|
for future in as_completed(future_to_chunk):
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
executor.shutdown(wait=False)
|
|
|
|
|
|
raise Exception("下载被取消")
|
|
|
|
|
|
|
|
|
|
|
|
result = future.result()
|
|
|
|
|
|
if result:
|
|
|
|
|
|
chunk_num, chunk_data = result
|
|
|
|
|
|
chunks_data[chunk_num] = chunk_data
|
|
|
|
|
|
successful_chunks += 1
|
|
|
|
|
|
logger.debug(f"分块 {chunk_num} 下载完成")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否所有分块都下载成功
|
|
|
|
|
|
if successful_chunks != num_chunks:
|
|
|
|
|
|
raise Exception(
|
|
|
|
|
|
f"下载不完整,成功分块: {successful_chunks}/{num_chunks}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 合并所有分块并写入文件
|
|
|
|
|
|
logger.info("开始合并分块数据")
|
|
|
|
|
|
with open(self.save_path, "wb") as f:
|
|
|
|
|
|
for chunk_data in chunks_data:
|
|
|
|
|
|
if chunk_data:
|
|
|
|
|
|
f.write(chunk_data)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"文件下载完成: {self.save_path}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"下载过程中发生错误: {e}")
|
|
|
|
|
|
# 清理可能已创建的不完整文件
|
|
|
|
|
|
if os.path.exists(self.save_path):
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(self.save_path)
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""主下载逻辑"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("下载在开始前已被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"开始下载文件,ID: {self.file_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取下载URL
|
|
|
|
|
|
if not self._getDownloadUrl():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
logger.info("下载在获取URL后被取消")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 执行带进度显示的多线程下载
|
|
|
|
|
|
self._downloadWithProgress()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否取消
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.downloadCancelled.emit()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 下载成功
|
|
|
|
|
|
self.downloadFinished.emit()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if self._is_cancelled:
|
|
|
|
|
|
self.downloadCancelled.emit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = f"下载失败: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
self.downloadFailed.emit(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
def cancelDownload(self):
|
|
|
|
|
|
"""取消下载操作"""
|
|
|
|
|
|
logger.info("取消下载请求")
|
|
|
|
|
|
self._is_cancelled = True
|