fix lot of thing
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from .basicApi import MiaoStarsBasicApi
|
||||
from .basicApi import MiaoStarsBasicApi, basicApi
|
||||
from ..utils.config import userConfig
|
||||
|
||||
miaoStarsBasicApi = MiaoStarsBasicApi()
|
||||
|
||||
@@ -371,15 +371,17 @@ class MiaoStarsBasicApi:
|
||||
if path == "/" or path == "":
|
||||
uri = "cloudreve://my/"
|
||||
else:
|
||||
# 清理路径,确保不包含cloudreve://my前缀
|
||||
normalized_path = path.strip("/").replace("\\", "/").replace("cloudreve://my/", "")
|
||||
# 使用quote_plus正确编码路径,确保特殊字符被正确处理
|
||||
# 首先规范化路径分隔符
|
||||
normalized_path = path.strip("/").replace("\\", "/")
|
||||
# 对路径部分进行URL编码
|
||||
encoded_path = quote_plus(normalized_path)
|
||||
# 由于quote_plus会将斜杠也编码,我们需要恢复它们
|
||||
encoded_path = encoded_path.replace("%2F", "/")
|
||||
uri = f"cloudreve://my/{encoded_path}"
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
uri = uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
|
||||
# 添加必需的分页参数
|
||||
params = {
|
||||
"uri": uri,
|
||||
@@ -403,12 +405,12 @@ class MiaoStarsBasicApi:
|
||||
|
||||
def getPolicy(self):
|
||||
"""获取用户存储策略"""
|
||||
url = "/user/policies"
|
||||
url = "/user/setting/policies"
|
||||
r = self.request("GET", url)
|
||||
|
||||
# 转换响应格式
|
||||
if isinstance(r, list):
|
||||
return {"code": 0, "data": r}
|
||||
return r
|
||||
else:
|
||||
return {"code": 0, "data": []}
|
||||
|
||||
@@ -427,11 +429,28 @@ class MiaoStarsBasicApi:
|
||||
"""创建文件夹"""
|
||||
url = "/file/create"
|
||||
currentPath = policyConfig.returnCurrentPath()
|
||||
# 根据Cloudreve V4 API规范构建uri参数,确保不会有重复斜杠
|
||||
if currentPath == "/":
|
||||
# 根据Cloudreve V4 API规范构建uri参数,确保不会有重复斜杠和前缀
|
||||
# 清理currentPath,确保不包含cloudreve://my前缀
|
||||
clean_current_path = currentPath.replace("cloudreve://my", "")
|
||||
|
||||
if clean_current_path == "/":
|
||||
uri = f"cloudreve://my/{name}"
|
||||
else:
|
||||
uri = f"cloudreve://my{currentPath}/{name}"
|
||||
uri = f"cloudreve://my{clean_current_path}/{name}"
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
uri = uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
|
||||
# 更健壮地处理重复文件名的情况
|
||||
path_parts = uri.split('/')
|
||||
if len(path_parts) > 1:
|
||||
# 检查最后一个部分是否是名称
|
||||
if path_parts[-1] == name:
|
||||
# 检查倒数第二个部分是否也是名称
|
||||
if len(path_parts) > 2 and path_parts[-2] == name:
|
||||
# 移除重复的名称部分
|
||||
path_parts.pop(-2)
|
||||
uri = '/'.join(path_parts)
|
||||
r = self.request("POST", url, json={"uri": uri, "type": "folder", "err_on_conflict": True})
|
||||
|
||||
# 转换响应格式
|
||||
@@ -440,20 +459,26 @@ class MiaoStarsBasicApi:
|
||||
else:
|
||||
return {"code": -1, "msg": r.get("msg", "文件夹创建失败")}
|
||||
|
||||
def deleteFile(self, fileId, fileType: Literal["file", "dir"]):
|
||||
"""删除文件"""
|
||||
url = "/file/delete"
|
||||
def deleteFile(self, fileUri, fileType: Literal["file", "dir"]):
|
||||
"""删除文件 (Cloudreve V4 API)"""
|
||||
url = "/file"
|
||||
|
||||
# 现在调用方已经传入了正确格式的URI
|
||||
logger.debug(f"删除文件,URI: {fileUri}")
|
||||
|
||||
# 根据Cloudreve V4 API规范,使用uris参数列表
|
||||
deleteData = {
|
||||
"items": [fileId] if fileType == "file" else [],
|
||||
"dirs": [fileId] if fileType == "dir" else []
|
||||
"uris": [fileUri]
|
||||
}
|
||||
r = self.request("POST", url, json=deleteData)
|
||||
r = self.request("DELETE", url, json=deleteData)
|
||||
|
||||
# 转换响应格式
|
||||
if r.get("count") > 0:
|
||||
# Cloudreve V4 响应格式:{code: 0} 表示成功
|
||||
if isinstance(r, dict) and r.get("code") == 0:
|
||||
return {"code": 0, "msg": "删除成功"}
|
||||
else:
|
||||
return {"code": -1, "msg": r.get("error", "删除失败")}
|
||||
error_msg = r.get("msg", r.get("error", "删除失败"))
|
||||
return {"code": -1, "msg": error_msg}
|
||||
|
||||
def wareSearch(
|
||||
self,
|
||||
@@ -564,6 +589,213 @@ class MiaoStarsBasicApi:
|
||||
else:
|
||||
return {"code": -1, "msg": r.get("error", "文件内容更新失败")}
|
||||
|
||||
def getFileUrl(self, fileUri, redirect=False):
|
||||
"""获取文件临时下载URL (Cloudreve V4 API)"""
|
||||
url = "/file/url"
|
||||
|
||||
# 确保传入的是正确格式的URI
|
||||
if not fileUri.startswith("cloudreve://"):
|
||||
logger.error(f"无效的URI格式: {fileUri},必须以cloudreve://开头")
|
||||
return {"code": -1, "msg": "无效的URI格式"}
|
||||
|
||||
# 修复文件名重复问题 - 保留URI编码文件名,删除后面的原始文件名
|
||||
if fileUri and '/' in fileUri:
|
||||
import urllib.parse
|
||||
parts = fileUri.split('/')
|
||||
|
||||
# 检查是否存在潜在的文件名重复情况
|
||||
if len(parts) >= 2:
|
||||
# 解码最后一个部分,检查是否与倒数第二个部分解码后相同
|
||||
try:
|
||||
decoded_last = urllib.parse.unquote(parts[-1])
|
||||
decoded_prev = urllib.parse.unquote(parts[-2])
|
||||
|
||||
# 如果解码后相同,或者最后一个部分是原始文件名(没有编码)
|
||||
if decoded_last == decoded_prev or parts[-1] == decoded_prev:
|
||||
# 保留编码的文件名部分,移除后面的重复部分
|
||||
fileUri = '/'.join(parts[:-1])
|
||||
logger.debug(f"修复了重复文件名的URI: {fileUri}")
|
||||
except Exception as e:
|
||||
logger.debug(f"解码URI部分时出错: {str(e)}")
|
||||
|
||||
data = {
|
||||
"uris": [fileUri],
|
||||
"redirect": redirect,
|
||||
"download": False # 设置为False以获取预览URL
|
||||
}
|
||||
logger.debug(f"获取文件URL,URI: {fileUri}")
|
||||
r = self.request("POST", url, json=data)
|
||||
|
||||
# 转换响应格式
|
||||
# 直接返回API响应,让调用者处理不同的返回格式
|
||||
logger.debug(f"getFileUrl API响应: {r}")
|
||||
|
||||
# 如果是成功的响应格式,确保返回完整的响应对象
|
||||
if isinstance(r, dict):
|
||||
# 兼容不同的成功响应格式
|
||||
if "urls" in r and isinstance(r["urls"], list):
|
||||
# 清理URL中的反引号和多余空格
|
||||
for url_item in r["urls"]:
|
||||
if isinstance(url_item, dict) and "url" in url_item:
|
||||
url_item["url"] = url_item["url"].strip('` ')
|
||||
return r
|
||||
elif r.get("code") == 0 and "data" in r:
|
||||
# Cloudreve标准格式
|
||||
return r
|
||||
elif r.get("code") != 0:
|
||||
# 错误响应
|
||||
return r
|
||||
|
||||
# 如果响应不符合预期格式,返回错误
|
||||
logger.error(f"getFileUrl响应格式不符合预期: {r}")
|
||||
return {"code": -1, "msg": "获取文件URL失败,响应格式错误"}
|
||||
|
||||
def getFileContent(self, fileUri):
|
||||
"""获取文件内容 (Cloudreve V4 API - PUT /file/content)"""
|
||||
url = "/file/content"
|
||||
|
||||
# 确保传入的是正确格式的URI
|
||||
if not fileUri.startswith("cloudreve://"):
|
||||
logger.error(f"无效的URI格式: {fileUri},必须以cloudreve://开头")
|
||||
return {"code": -1, "msg": "无效的URI格式"}
|
||||
|
||||
# 修复文件名重复问题 - 保留URI编码文件名,删除后面的原始文件名
|
||||
if fileUri and '/' in fileUri:
|
||||
import urllib.parse
|
||||
parts = fileUri.split('/')
|
||||
|
||||
# 检查是否存在潜在的文件名重复情况
|
||||
if len(parts) >= 2:
|
||||
# 解码最后一个部分,检查是否与倒数第二个部分解码后相同
|
||||
try:
|
||||
decoded_last = urllib.parse.unquote(parts[-1])
|
||||
decoded_prev = urllib.parse.unquote(parts[-2])
|
||||
|
||||
# 如果解码后相同,或者最后一个部分是原始文件名(没有编码)
|
||||
if decoded_last == decoded_prev or parts[-1] == decoded_prev:
|
||||
# 保留编码的文件名部分,移除后面的重复部分
|
||||
fileUri = '/'.join(parts[:-1])
|
||||
logger.debug(f"getFileContent - 修复了重复文件名的URI: {fileUri}")
|
||||
except Exception as e:
|
||||
logger.debug(f"解码URI部分时出错: {str(e)}")
|
||||
|
||||
# 构建查询参数
|
||||
params = {"uri": fileUri}
|
||||
|
||||
logger.debug(f"获取文件内容,URI: {fileUri}")
|
||||
|
||||
try:
|
||||
# 使用PUT请求获取文件内容
|
||||
r = self.request("PUT", url, params=params)
|
||||
|
||||
# 转换响应格式
|
||||
if isinstance(r, bytes):
|
||||
# 直接返回二进制内容
|
||||
return {"code": 0, "data": r}
|
||||
elif isinstance(r, dict):
|
||||
if r.get("code") == 0:
|
||||
# 确保返回的数据是二进制格式
|
||||
data = r.get("data")
|
||||
if isinstance(data, bytes):
|
||||
return {"code": 0, "data": data}
|
||||
elif isinstance(data, dict) or isinstance(data, list):
|
||||
# 如果data是字典或列表,说明可能是API响应错误,使用URL方式获取
|
||||
logger.warning(f"获取到的数据是{type(data).__name__}类型,不是二进制数据,将尝试使用URL方式")
|
||||
|
||||
# 尝试使用getFileUrl获取临时URL,然后下载内容
|
||||
url_response = self.getFileUrl(fileUri)
|
||||
if url_response.get("code") == 0 and "data" in url_response and "urls" in url_response["data"]:
|
||||
urls = url_response["data"]["urls"]
|
||||
if urls and isinstance(urls[0], dict) and "url" in urls[0]:
|
||||
temp_url = urls[0]["url"]
|
||||
# 使用session获取文件内容
|
||||
session_response = self.returnSession().get(temp_url, stream=True, timeout=(15, 30))
|
||||
session_response.raise_for_status()
|
||||
binary_content = session_response.content
|
||||
logger.info(f"成功从临时URL获取文件内容,大小: {len(binary_content)} 字节")
|
||||
return {"code": 0, "data": binary_content}
|
||||
else:
|
||||
# 尝试将其他类型转换为二进制
|
||||
try:
|
||||
if isinstance(data, str):
|
||||
return {"code": 0, "data": data.encode('utf-8')}
|
||||
else:
|
||||
return {"code": 0, "data": str(data).encode('utf-8')}
|
||||
except Exception:
|
||||
pass
|
||||
# 错误响应
|
||||
return {"code": r.get("code", -1), "msg": r.get("msg", "获取文件内容失败")}
|
||||
else:
|
||||
# 未知响应类型,尝试转换为二进制
|
||||
try:
|
||||
return {"code": 0, "data": str(r).encode('utf-8')}
|
||||
except Exception:
|
||||
return {"code": -1, "msg": f"未知的响应类型: {type(r).__name__}"}
|
||||
except Exception as e:
|
||||
logger.error(f"获取文件内容时发生异常: {str(e)}")
|
||||
return {"code": -1, "msg": f"获取文件内容失败: {str(e)}"}
|
||||
|
||||
def updateFileContent(self, fileUri, content, previous_version=None):
|
||||
"""更新文件内容 (Cloudreve V4 API - PUT /file/content)"""
|
||||
url = "/file/content"
|
||||
|
||||
# 确保传入的是正确格式的URI
|
||||
if not fileUri.startswith("cloudreve://"):
|
||||
logger.error(f"无效的URI格式: {fileUri},必须以cloudreve://开头")
|
||||
return {"code": -1, "msg": "无效的URI格式"}
|
||||
|
||||
# 构建查询参数
|
||||
params = {"uri": fileUri}
|
||||
if previous_version:
|
||||
params["previous"] = previous_version
|
||||
|
||||
# 设置请求头
|
||||
headers = {
|
||||
"Content-Length": str(len(content))
|
||||
}
|
||||
|
||||
logger.debug(f"更新文件内容,URI: {fileUri}")
|
||||
r = self.request("PUT", url, params=params, data=content, headers=headers)
|
||||
|
||||
# 转换响应格式
|
||||
if isinstance(r, dict) and r.get("code") == 0:
|
||||
return {"code": 0, "data": r.get("data", {})}
|
||||
else:
|
||||
error_msg = r.get("msg", r.get("error", "更新文件内容失败"))
|
||||
return {"code": -1, "msg": error_msg}
|
||||
|
||||
def createViewerSession(self, fileUri, viewer_id, preferred_action="view", version=None, parent_uri=None):
|
||||
"""创建查看器会话 (Cloudreve V4 API - PUT /file/viewerSession)"""
|
||||
url = "/file/viewerSession"
|
||||
|
||||
# 确保传入的是正确格式的URI
|
||||
if not fileUri.startswith("cloudreve://"):
|
||||
logger.error(f"无效的URI格式: {fileUri},必须以cloudreve://开头")
|
||||
return {"code": -1, "msg": "无效的URI格式"}
|
||||
|
||||
# 构建请求体
|
||||
data = {
|
||||
"uri": fileUri,
|
||||
"viewer_id": viewer_id,
|
||||
"preferred_action": preferred_action
|
||||
}
|
||||
|
||||
# 添加可选参数
|
||||
if version:
|
||||
data["version"] = version
|
||||
if parent_uri:
|
||||
data["parent_uri"] = parent_uri
|
||||
|
||||
logger.debug(f"创建查看器会话,URI: {fileUri}, viewer_id: {viewer_id}")
|
||||
r = self.request("PUT", url, json=data)
|
||||
|
||||
# 转换响应格式
|
||||
if isinstance(r, dict) and r.get("code") == 0:
|
||||
return {"code": 0, "data": r.get("data", {})}
|
||||
else:
|
||||
error_msg = r.get("msg", r.get("error", "创建查看器会话失败"))
|
||||
return {"code": -1, "msg": error_msg}
|
||||
|
||||
def updateUserNickname(self, nickName):
|
||||
"""更新用户昵称 (Cloudreve V4 API)"""
|
||||
url = "/user/profile"
|
||||
@@ -577,3 +809,7 @@ class MiaoStarsBasicApi:
|
||||
return {"code": 0, "msg": "昵称更新成功"}
|
||||
else:
|
||||
return {"code": -1, "msg": r.get("error", "昵称更新失败")}
|
||||
|
||||
|
||||
# 创建全局实例,方便其他模块直接使用
|
||||
basicApi = MiaoStarsBasicApi()
|
||||
|
||||
@@ -140,22 +140,22 @@ class DeleteFileThread(QThread):
|
||||
successDelete = pyqtSignal()
|
||||
errorDelete = pyqtSignal(str)
|
||||
|
||||
def __init__(self, fileId: str, fileType: str):
|
||||
def __init__(self, fileUri: str, fileType: str):
|
||||
super().__init__()
|
||||
logger.debug(f"初始化删除文件线程 - ID: {fileId}, 类型: {fileType}")
|
||||
self.fileId = fileId
|
||||
logger.debug(f"初始化删除文件线程 - URI: {fileUri}, 类型: {fileType}")
|
||||
self.fileUri = fileUri
|
||||
self.fileType = fileType
|
||||
|
||||
def run(self):
|
||||
logger.info(f"开始删除文件 - ID: {self.fileId}, 类型: {self.fileType}")
|
||||
logger.info(f"开始删除文件 - URI: {self.fileUri}, 类型: {self.fileType}")
|
||||
try:
|
||||
response = miaoStarsBasicApi.deleteFile(self.fileId, self.fileType)
|
||||
response = miaoStarsBasicApi.deleteFile(self.fileUri, self.fileType)
|
||||
if response["code"] == 0:
|
||||
logger.info(f"文件删除成功 - ID: {self.fileId}")
|
||||
logger.info(f"文件删除成功 - URI: {self.fileUri}")
|
||||
self.successDelete.emit()
|
||||
else:
|
||||
logger.error(
|
||||
f"文件删除失败 - ID: {self.fileId}, 错误: {response.get('msg', '未知错误')}"
|
||||
f"文件删除失败 - URI: {self.fileUri}, 错误: {response.get('msg', '未知错误')}"
|
||||
)
|
||||
self.errorDelete.emit(f"删除失败: {response.get('msg', '未知错误')}")
|
||||
|
||||
@@ -237,7 +237,11 @@ class UploadThread(QThread):
|
||||
self.applicationUrl = "/file/upload"
|
||||
|
||||
self.current_path = policyConfig.returnCurrentPath()
|
||||
self.policy = policyConfig.returnPolicy().get("id")
|
||||
# 获取存储策略,如果为None则使用默认值PqI8
|
||||
self.policy = policyConfig.returnPolicy().get("id", "PqI8")
|
||||
if self.policy is None:
|
||||
self.policy = "PqI8"
|
||||
logger.info("存储策略ID为None,已设置默认值PqI8")
|
||||
|
||||
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",
|
||||
@@ -262,54 +266,99 @@ class UploadThread(QThread):
|
||||
return mime_type if mime_type else "application/octet-stream"
|
||||
|
||||
def _prepareUploadData(self) -> Dict[str, Any]:
|
||||
"""准备上传数据"""
|
||||
"""准备上传数据,符合Cloudreve V4 API规范"""
|
||||
try:
|
||||
modification_time = os.path.getmtime(self.file_path)
|
||||
size = os.path.getsize(self.file_path)
|
||||
|
||||
# 构建符合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}")
|
||||
|
||||
return {
|
||||
"path": self.current_path,
|
||||
"uri": uri,
|
||||
"size": size,
|
||||
"name": self.file_name,
|
||||
"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}")
|
||||
raise
|
||||
|
||||
def _uploadWithProgress(self, upload_url: str, credential: str, total_size: int):
|
||||
"""带进度显示的上传方法"""
|
||||
try:
|
||||
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}
|
||||
|
||||
# 打开文件
|
||||
logger.info(f"开始打开文件: {self.file_path}")
|
||||
self._file_obj = open(self.file_path, "rb")
|
||||
logger.info("文件打开成功")
|
||||
|
||||
# 准备上传头信息
|
||||
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": "*/*",
|
||||
"Authorization": credential,
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"Content-Type": self.getMimeType(self.file_path), # 使用正确的MIME类型
|
||||
"Content-Length": str(total_size), # 添加必需的Content-Length头
|
||||
# 删除硬编码的Origin和Referer,使用miaoStarsBasicApi中已配置的请求头
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
**auth_header, # 只在credential不为None时添加Authorization头
|
||||
}
|
||||
logger.info(f"上传头信息准备完成: {upload_headers}")
|
||||
|
||||
# 发送预检请求
|
||||
options_result = miaoStarsBasicApi.returnSession().options(
|
||||
upload_url, headers=upload_headers
|
||||
)
|
||||
options_result.raise_for_status()
|
||||
logger.info("OPTIONS预检请求完成")
|
||||
# 尝试发送预检请求,但不阻塞主流程
|
||||
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)}")
|
||||
|
||||
# 创建自定义请求体以支持进度监控和取消
|
||||
class ProgressFileObject:
|
||||
def __init__(
|
||||
self, file_obj, total_size, progress_callback, cancel_check
|
||||
):
|
||||
logger.info("创建ProgressFileObject对象")
|
||||
self.file_obj = file_obj
|
||||
self.total_size = total_size
|
||||
self.progress_callback = progress_callback
|
||||
@@ -319,7 +368,8 @@ class UploadThread(QThread):
|
||||
def read(self, size=-1):
|
||||
# 检查是否取消
|
||||
if self.cancel_check():
|
||||
return
|
||||
logger.info("检测到取消上传请求")
|
||||
raise InterruptedError("Upload cancelled by user")
|
||||
|
||||
data = self.file_obj.read(size)
|
||||
if data:
|
||||
@@ -330,12 +380,14 @@ class UploadThread(QThread):
|
||||
else 0
|
||||
)
|
||||
self.progress_callback(progress, self.uploaded, self.total_size)
|
||||
logger.debug(f"上传进度: {progress:.2f}% ({self.uploaded}/{self.total_size} 字节)")
|
||||
return data
|
||||
|
||||
def __len__(self):
|
||||
return self.total_size
|
||||
|
||||
# 创建带进度监控的文件对象
|
||||
logger.info("创建进度监控文件对象")
|
||||
progress_file = ProgressFileObject(
|
||||
self._file_obj,
|
||||
total_size,
|
||||
@@ -347,17 +399,60 @@ class UploadThread(QThread):
|
||||
|
||||
# 执行实际上传
|
||||
logger.info(f"开始上传文件,总大小: {total_size} 字节")
|
||||
upload_result = miaoStarsBasicApi.returnSession().post(
|
||||
upload_url,
|
||||
data=progress_file,
|
||||
headers=upload_headers,
|
||||
timeout=60,
|
||||
)
|
||||
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 }}")
|
||||
|
||||
logger.info(f"上传完成,响应状态: {upload_result.status_code}")
|
||||
upload_result.raise_for_status()
|
||||
# 修复:优化上传逻辑,先尝试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
|
||||
)
|
||||
|
||||
return upload_result
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"上传过程中发生错误: {e}")
|
||||
@@ -380,6 +475,24 @@ class UploadThread(QThread):
|
||||
# 准备上传数据
|
||||
upload_data = self._prepareUploadData()
|
||||
|
||||
# 检查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'
|
||||
|
||||
# 检查是否取消
|
||||
if self._is_cancelled:
|
||||
logger.info("上传在获取上传URL前被取消")
|
||||
@@ -387,41 +500,126 @@ class UploadThread(QThread):
|
||||
|
||||
# 执行上传请求获取上传URL
|
||||
logger.info("请求上传URL")
|
||||
# 从miaoStarsBasicApi获取完整URL(去掉/api/v4后缀)
|
||||
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4') # 使用API类中定义的基础URL
|
||||
full_url = f"{base_url}{self.applicationUrl}"
|
||||
# 直接使用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}")
|
||||
|
||||
response = miaoStarsBasicApi.returnSession().put(
|
||||
full_url, json=upload_data, headers=self.headers, timeout=30
|
||||
)
|
||||
|
||||
# 记录响应状态码和内容
|
||||
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}")
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
# 添加错误处理,检查响应是否为有效的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}")
|
||||
|
||||
if result.get("code") == 0:
|
||||
self.uploadApplicationApprovedSignal.emit()
|
||||
upload_urls = result.get("data").get("uploadURLs")
|
||||
credential = result.get("data").get("credential")
|
||||
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", "")
|
||||
|
||||
self.uploadUrl = upload_urls[0] + "?chunk=0"
|
||||
logger.info("获取到上传URL")
|
||||
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}")
|
||||
|
||||
# 获取文件总大小
|
||||
total_size = os.path.getsize(self.file_path)
|
||||
|
||||
# 执行带进度显示的上传
|
||||
upload_result = self._uploadWithProgress(
|
||||
self.uploadUrl, credential, total_size
|
||||
)
|
||||
logger.info("开始调用_uploadWithProgress方法执行上传")
|
||||
try:
|
||||
upload_result = self._uploadWithProgress(
|
||||
self.uploadUrl, credential, total_size
|
||||
)
|
||||
logger.info("_uploadWithProgress方法调用完成")
|
||||
|
||||
# 检查是否取消
|
||||
if self._is_cancelled:
|
||||
self.uploadCancelled.emit()
|
||||
# 检查是否取消
|
||||
if self._is_cancelled:
|
||||
self.uploadCancelled.emit()
|
||||
return
|
||||
|
||||
# 处理上传结果
|
||||
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
|
||||
|
||||
# 处理上传结果
|
||||
logger.info(f"上传响应: {upload_result}")
|
||||
self.uploadFinished.emit()
|
||||
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()
|
||||
|
||||
else:
|
||||
error_msg = result.get("msg", "上传失败")
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QImage, QPixmap
|
||||
|
||||
from app.core import miaoStarsBasicApi
|
||||
from app.core.api import basicApi, miaoStarsBasicApi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextLoaderThread(QThread):
|
||||
@@ -15,54 +18,88 @@ class TextLoaderThread(QThread):
|
||||
errorOccurred = pyqtSignal(str)
|
||||
progressUpdated = pyqtSignal(int) # 进度更新信号
|
||||
|
||||
def __init__(self, url):
|
||||
def __init__(self, url, fileId=None):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.url = url # 原始URL,可能作为fallback
|
||||
self.fileId = fileId # 文件ID,优先使用
|
||||
|
||||
def run(self):
|
||||
"""线程执行函数"""
|
||||
try:
|
||||
# 1. 设置网络请求参数 - 优化连接参数
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=20,
|
||||
pool_maxsize=20,
|
||||
max_retries=5, # 增加重试次数
|
||||
pool_block=False,
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
binary_content = None
|
||||
content_chunks = [] # 预先初始化content_chunks,避免在任何执行路径下出现未定义错误
|
||||
|
||||
# 2. 增加超时时间并添加重试机制
|
||||
response = miaoStarsBasicApi.returnSession().get(
|
||||
self.url,
|
||||
stream=True,
|
||||
timeout=(15, 30), # 增加超时时间:连接15秒,读取30秒
|
||||
)
|
||||
# 如果提供了fileId(实际上是URI),优先使用getFileContent直接获取内容
|
||||
if self.fileId:
|
||||
self.progressUpdated.emit(0) # 发送初始进度
|
||||
logger.info(f"使用文件URI {self.fileId} 直接获取文件内容")
|
||||
response = basicApi.getFileContent(self.fileId)
|
||||
|
||||
response.raise_for_status()
|
||||
if response.get('code') == 0 and 'data' in response:
|
||||
# 检查response['data']的类型,确保只有二进制数据才直接使用
|
||||
if isinstance(response['data'], bytes):
|
||||
binary_content = response['data']
|
||||
logger.info(f"成功获取文件内容,大小: {len(binary_content)} 字节")
|
||||
self.progressUpdated.emit(100) # 直接发送完成进度
|
||||
else:
|
||||
# 如果不是二进制数据(可能是字典等其他类型),记录警告并使用URL作为fallback
|
||||
logger.warning(f"获取到的文件内容不是二进制数据,类型: {type(response['data']).__name__},将使用URL作为fallback")
|
||||
binary_content = None
|
||||
else:
|
||||
error_msg = response.get('msg', '获取文件内容失败')
|
||||
logger.warning(f"获取文件内容失败: {error_msg},将使用原始URL作为fallback")
|
||||
|
||||
# 3. 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
# 如果直接获取内容失败,尝试使用URL
|
||||
if binary_content is None:
|
||||
if not self.url:
|
||||
self.errorOccurred.emit("没有可用的URL来加载文本内容")
|
||||
return
|
||||
|
||||
# 4. 分块读取并处理 - 使用二进制读取提高速度
|
||||
content_chunks = []
|
||||
for chunk in response.iter_content(chunk_size=16384): # 增大块大小
|
||||
if chunk:
|
||||
content_chunks.append(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
# 1. 设置网络请求参数 - 优化连接参数
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=20,
|
||||
pool_maxsize=20,
|
||||
max_retries=5, # 增加重试次数
|
||||
pool_block=False,
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
# 2. 增加超时时间并添加重试机制
|
||||
response = basicApi.returnSession().get(
|
||||
self.url,
|
||||
stream=True,
|
||||
timeout=(15, 30), # 增加超时时间:连接15秒,读取30秒
|
||||
)
|
||||
|
||||
# 5. 合并内容并解码
|
||||
binary_content = b"".join(content_chunks)
|
||||
response.raise_for_status()
|
||||
|
||||
if not binary_content:
|
||||
self.errorOccurred.emit("下载内容为空")
|
||||
# 3. 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 4. 分块读取并处理 - 使用二进制读取提高速度
|
||||
content_chunks = []
|
||||
for chunk in response.iter_content(chunk_size=16384): # 增大块大小
|
||||
if chunk:
|
||||
content_chunks.append(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
|
||||
# 5. 合并内容 - 使用正确的二进制合并语法
|
||||
binary_content = b"" .join(content_chunks)
|
||||
|
||||
# 确保binary_content是bytes类型且不为空
|
||||
if not binary_content or not isinstance(binary_content, bytes):
|
||||
if not binary_content:
|
||||
self.errorOccurred.emit("下载内容为空")
|
||||
else:
|
||||
self.errorOccurred.emit(f"下载内容类型错误,应为bytes,实际为: {type(binary_content).__name__}")
|
||||
return
|
||||
|
||||
# 6. 智能编码检测和解码
|
||||
@@ -113,10 +150,11 @@ class ImageLoaderThread(QThread):
|
||||
progressUpdated = pyqtSignal(int) # 进度更新信号
|
||||
|
||||
def __init__(
|
||||
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024
|
||||
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024, fileId=None
|
||||
): # 50MB缓存
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.url = url # 原始URL,可能作为fallback
|
||||
self.fileId = fileId # 文件ID,优先使用
|
||||
self.cache_dir = cache_dir
|
||||
self.max_cache_size = max_cache_size
|
||||
self._setup_cache()
|
||||
@@ -184,48 +222,108 @@ class ImageLoaderThread(QThread):
|
||||
def run(self):
|
||||
"""线程执行函数"""
|
||||
try:
|
||||
# 1. 首先检查缓存
|
||||
cached_pixmap = self._get_cached_image()
|
||||
if cached_pixmap:
|
||||
self.imageLoaded.emit(cached_pixmap)
|
||||
return
|
||||
image_data = None
|
||||
|
||||
# 2. 设置更短的超时时间
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
# 如果提供了fileId(实际上是URI),优先使用getFileContent直接获取内容
|
||||
if self.fileId:
|
||||
logger.info(f"使用文件URI {self.fileId} 直接获取图片内容")
|
||||
response = miaoStarsBasicApi.getFileContent(self.fileId)
|
||||
|
||||
# 3. 流式下载,支持进度显示
|
||||
response = miaoStarsBasicApi.returnSession().get(
|
||||
self.url, stream=True, timeout=(20, 30) # 连接超时5秒,读取超时10秒
|
||||
)
|
||||
response.raise_for_status()
|
||||
if response.get('code') == 0 and 'data' in response:
|
||||
# 检查response['data']的类型,确保只有二进制数据才直接使用
|
||||
if isinstance(response['data'], bytes):
|
||||
image_data = response['data']
|
||||
logger.info(f"成功获取图片内容,大小: {len(image_data)} 字节")
|
||||
self.progressUpdated.emit(100) # 直接发送完成进度
|
||||
else:
|
||||
# 如果不是二进制数据(可能是字典等其他类型),记录警告并使用URL作为fallback
|
||||
logger.warning(f"获取到的图片内容不是二进制数据,类型: {type(response['data']).__name__},将使用URL作为fallback")
|
||||
else:
|
||||
error_msg = response.get('msg', '获取图片内容失败')
|
||||
logger.warning(f"获取图片内容失败: {error_msg},将使用原始URL作为fallback")
|
||||
|
||||
# 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
# 如果直接获取内容失败,尝试使用URL
|
||||
if image_data is None:
|
||||
if not self.url:
|
||||
self.errorOccurred.emit("没有可用的URL来加载图片内容")
|
||||
return
|
||||
|
||||
# 4. 分块读取并处理
|
||||
image_data = b""
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
image_data += chunk
|
||||
downloaded_size += len(chunk)
|
||||
# 确保URL是字符串类型
|
||||
if isinstance(self.url, dict):
|
||||
# 处理字典类型的URL,通常包含urls数组
|
||||
if 'urls' in self.url and isinstance(self.url['urls'], list) and len(self.url['urls']) > 0:
|
||||
# 获取第一个URL信息
|
||||
url_info = self.url['urls'][0]
|
||||
if isinstance(url_info, dict) and 'url' in url_info:
|
||||
download_url = url_info['url']
|
||||
logger.info(f"从字典中提取URL: {download_url}")
|
||||
self.url = download_url
|
||||
else:
|
||||
logger.error("URL字典格式不正确,缺少有效的URL信息")
|
||||
self.errorOccurred.emit("URL格式错误: 缺少有效的URL信息")
|
||||
return
|
||||
else:
|
||||
logger.error(f"URL字典格式不正确,缺少urls数组或数组为空: {self.url}")
|
||||
self.errorOccurred.emit("URL格式错误: 缺少有效的URL信息")
|
||||
return
|
||||
elif not isinstance(self.url, str):
|
||||
logger.error(f"URL类型错误,应为字符串或字典,实际为: {type(self.url).__name__}")
|
||||
self.errorOccurred.emit(f"URL类型错误: 应为字符串")
|
||||
return
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
# 1. 首先检查缓存
|
||||
cached_pixmap = self._get_cached_image()
|
||||
if cached_pixmap:
|
||||
self.imageLoaded.emit(cached_pixmap)
|
||||
return
|
||||
|
||||
# 2. 设置更短的超时时间
|
||||
session = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# 3. 流式下载,支持进度显示
|
||||
response = miaoStarsBasicApi.returnSession().get(
|
||||
self.url, stream=True, timeout=(20, 30) # 连接超时5秒,读取超时10秒
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# 获取文件大小(如果服务器支持)
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 4. 分块读取并处理
|
||||
image_data = b""
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
image_data += chunk
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度(如果知道总大小)
|
||||
if total_size > 0:
|
||||
# 确保进度不超过100%
|
||||
progress = min(int((downloaded_size / total_size) * 100), 100)
|
||||
self.progressUpdated.emit(progress)
|
||||
|
||||
# 5. 从数据创建QImage(比QPixmap更快)
|
||||
image = QImage()
|
||||
image.loadFromData(image_data)
|
||||
|
||||
if image.isNull():
|
||||
raise Exception("无法加载图片数据")
|
||||
# 检查数据是否为空或类型是否正确
|
||||
if not image_data:
|
||||
raise Exception("图片数据为空")
|
||||
|
||||
if not isinstance(image_data, bytes):
|
||||
logger.error(f"图片数据类型错误,应为bytes,实际为: {type(image_data).__name__}")
|
||||
raise Exception(f"图片数据类型错误: 应为bytes,实际为{type(image_data).__name__}")
|
||||
|
||||
# 尝试加载图片数据
|
||||
load_success = image.loadFromData(image_data)
|
||||
|
||||
if not load_success or image.isNull():
|
||||
raise Exception("无法加载图片数据或图片格式不支持")
|
||||
|
||||
# 6. 转换为QPixmap
|
||||
pixmap = QPixmap.fromImage(image)
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
Version information for the application
|
||||
"""
|
||||
|
||||
version = "0.0.2"
|
||||
version = "Alpha 0.1"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 9.3 MiB |
BIN
app/resource/images/bg6.png
Normal file
BIN
app/resource/images/bg6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
BIN
app/resource/images/title.png
Normal file
BIN
app/resource/images/title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
26226
app/resource/resource.py
26226
app/resource/resource.py
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<file>images/load.png</file>
|
||||
<file>images/error.png</file>
|
||||
<file>images/empty.png</file>
|
||||
<file>images/title.jpg</file>
|
||||
<file>images/title.png</file>
|
||||
<file>images/loadFailure.png</file>
|
||||
<file>images/logolong.png</file>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class AppInfoInterface(QWidget):
|
||||
mainLayout.setContentsMargins(30, 30, 30, 30)
|
||||
mainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.titleImageLabel = ImageLabel(":app/images/title.jpg", self)
|
||||
self.titleImageLabel = ImageLabel(":app/images/title.png", self)
|
||||
self.titleImageLabel.scaledToHeight(130)
|
||||
mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
|
||||
@@ -95,14 +95,26 @@ class FileCard(CardWidget):
|
||||
full_path = f"cloudreve://my/{self.fileName}"
|
||||
else:
|
||||
# 子目录情况,确保正确拼接路径
|
||||
# 清理路径,避免重复的斜杠
|
||||
# 清理路径,避免重复的斜杠和前缀
|
||||
clean_path = self.filePath.lstrip("/")
|
||||
# 确保路径中不包含cloudreve://my前缀
|
||||
clean_path = clean_path.replace("cloudreve://my/", "")
|
||||
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
|
||||
# 确保路径格式正确,没有重复的部分
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
# 确保没有重复的文件名
|
||||
if f"/{self.fileName}/{self.fileName}" in full_path:
|
||||
full_path = full_path.replace(f"/{self.fileName}/{self.fileName}", f"/{self.fileName}")
|
||||
|
||||
# 更健壮地处理重复文件名的情况
|
||||
# 分割路径并去重
|
||||
path_parts = full_path.split('/')
|
||||
if len(path_parts) > 1:
|
||||
# 检查最后一个部分是否是文件名
|
||||
if path_parts[-1] == self.fileName:
|
||||
# 检查倒数第二个部分是否也是文件名
|
||||
if len(path_parts) > 2 and path_parts[-2] == self.fileName:
|
||||
# 移除重复的文件名部分
|
||||
path_parts.pop(-2)
|
||||
full_path = '/'.join(path_parts)
|
||||
|
||||
signalBus.addDownloadFileTask.emit(
|
||||
f"own.{self.suffix}", self.fileName, full_path
|
||||
@@ -158,7 +170,34 @@ class FileCard(CardWidget):
|
||||
parent=self.window(),
|
||||
)
|
||||
if w.exec():
|
||||
self.deleteThread = DeleteFileThread(self._id, self.fileType)
|
||||
# 构建Cloudreve V4 API所需的正确路径格式
|
||||
if self.filePath == "/":
|
||||
# 根目录情况
|
||||
full_path = f"cloudreve://my/{self.fileName}"
|
||||
else:
|
||||
# 子目录情况,确保正确拼接路径
|
||||
# 清理路径,避免重复的斜杠和前缀
|
||||
clean_path = self.filePath.lstrip("/")
|
||||
# 确保路径中不包含cloudreve://my前缀
|
||||
clean_path = clean_path.replace("cloudreve://my/", "")
|
||||
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
|
||||
# 更健壮地处理重复文件名的情况
|
||||
# 分割路径并去重
|
||||
path_parts = full_path.split('/')
|
||||
if len(path_parts) > 1:
|
||||
# 检查最后一个部分是否是文件名
|
||||
if path_parts[-1] == self.fileName:
|
||||
# 检查倒数第二个部分是否也是文件名
|
||||
if len(path_parts) > 2 and path_parts[-2] == self.fileName:
|
||||
# 移除重复的文件名部分
|
||||
path_parts.pop(-2)
|
||||
full_path = '/'.join(path_parts)
|
||||
|
||||
self.deleteThread = DeleteFileThread(full_path, self.fileType)
|
||||
self.deleteThread.successDelete.connect(self.deleteSuccess)
|
||||
self.deleteThread.errorDelete.connect(self.deleteError)
|
||||
self.deleteThread.start()
|
||||
@@ -201,6 +240,16 @@ class FileCard(CardWidget):
|
||||
"""重写上下文菜单事件,确保只有右键点击才会触发"""
|
||||
pass
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""重写鼠标双击事件,双击文件夹时进入文件夹"""
|
||||
if self.fileType == "dir":
|
||||
self.dirClicked()
|
||||
# 阻止事件继续传播,避免可能的干扰
|
||||
event.accept()
|
||||
else:
|
||||
# 非文件夹时调用父类处理
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
class ShareFileCard(CardWidget):
|
||||
def __init__(self, data, parent=None):
|
||||
@@ -360,3 +409,13 @@ class SharedFolderFileCard(CardWidget):
|
||||
)
|
||||
|
||||
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
"""重写鼠标双击事件,双击文件夹时进入文件夹"""
|
||||
if self.fileType == "dir":
|
||||
self.dirClicked()
|
||||
# 阻止事件继续传播,避免可能的干扰
|
||||
event.accept()
|
||||
else:
|
||||
# 非文件夹时调用父类处理
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
@@ -68,8 +68,37 @@ class LinkageSwitchingBase(ScrollArea):
|
||||
fileDate = data.get("created_at", data.get("date", ""))
|
||||
fileSize = data.get("size", 0)
|
||||
|
||||
# 构建符合Cloudreve V4 API要求的正确URI格式
|
||||
if filePath == "/" or filePath == "":
|
||||
# 根目录情况
|
||||
full_uri = f"cloudreve://my/{fileName}"
|
||||
else:
|
||||
# 子目录情况,确保正确拼接路径
|
||||
# 清理路径,避免重复的斜杠和前缀
|
||||
clean_path = filePath.lstrip("/")
|
||||
# 确保路径中不包含cloudreve://my前缀
|
||||
clean_path = clean_path.replace("cloudreve://my/", "")
|
||||
full_uri = f"cloudreve://my/{clean_path}/{fileName}"
|
||||
|
||||
# 确保路径格式正确,移除重复的前缀
|
||||
full_uri = full_uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
|
||||
|
||||
# 更健壮地处理重复文件名的情况
|
||||
# 分割路径并去重
|
||||
path_parts = full_uri.split('/')
|
||||
if len(path_parts) > 1:
|
||||
# 检查最后一个部分是否是文件名
|
||||
if path_parts[-1] == fileName:
|
||||
# 检查倒数第二个部分是否也是文件名
|
||||
if len(path_parts) > 2 and path_parts[-2] == fileName:
|
||||
# 移除重复的文件名部分
|
||||
path_parts.pop(-2)
|
||||
full_uri = '/'.join(path_parts)
|
||||
|
||||
logger.debug(f"构建文件URI: {full_uri}")
|
||||
|
||||
fileCard = FileCard(
|
||||
fileId,
|
||||
full_uri, # 传递完整的URI而不是文件ID
|
||||
fileName,
|
||||
fileType,
|
||||
filePath,
|
||||
|
||||
@@ -181,16 +181,15 @@ class MainWindow(CustomFluentWindow):
|
||||
# 窗口大小改变时更新背景
|
||||
|
||||
def imagePreview(self, _id):
|
||||
# 使用V4 API进行预览
|
||||
url = f"/file/preview/{_id}"
|
||||
self.previewBox = OptimizedPreviewBox(self, url)
|
||||
# 直接传递文件ID给OptimizedPreviewBox,让它内部使用getFileUrl获取临时URL
|
||||
self.previewBox = OptimizedPreviewBox(self, fileId=_id)
|
||||
if self.previewBox.exec():
|
||||
pass
|
||||
|
||||
def txtPreview(self, _id):
|
||||
# 使用V4 API获取内容
|
||||
url = f"/file/content/{_id}"
|
||||
self.previewBox = PreviewTextBox(self, url, _id)
|
||||
# 直接使用fileUri,不再构建URL
|
||||
# _id 应该是 cloudreve:// 格式的URI
|
||||
self.previewBox = PreviewTextBox(self, "", _id)
|
||||
if self.previewBox.exec():
|
||||
pass
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ def createThumbnail(pixmap, max_size=200):
|
||||
|
||||
|
||||
class OptimizedPreviewBox(MessageBoxBase):
|
||||
def __init__(self, parent=None, url=None):
|
||||
def __init__(self, parent=None, url=None, fileId=None):
|
||||
super().__init__(parent=parent)
|
||||
# 处理URL,确保它是完整的URL
|
||||
self.url = self._ensure_full_url(url)
|
||||
logger.info(f"初始化图片预览框,URL: {self.url}")
|
||||
self.fileId = fileId # 保存文件ID
|
||||
self.url = url # 暂存原始URL,可能需要作为fallback
|
||||
logger.info(f"初始化图片预览框,文件ID: {self.fileId}, 原始URL: {self.url}")
|
||||
self.widget.setMinimumSize(500, 500)
|
||||
|
||||
self.original_pixmap = None
|
||||
@@ -58,16 +58,9 @@ class OptimizedPreviewBox(MessageBoxBase):
|
||||
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
|
||||
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用优化的图片加载线程
|
||||
self.imageLoaderThread = ImageLoaderThread(self.url)
|
||||
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
|
||||
self.imageLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
|
||||
# 延迟启动加载,避免阻塞UI初始化
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(100, self.startLoading)
|
||||
QTimer.singleShot(100, self._initImageLoading)
|
||||
|
||||
def _ensure_full_url(self, url):
|
||||
"""确保URL是完整的,添加scheme和base URL(如果缺失)"""
|
||||
@@ -87,10 +80,94 @@ class OptimizedPreviewBox(MessageBoxBase):
|
||||
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
|
||||
return full_url
|
||||
|
||||
def startLoading(self):
|
||||
"""开始加载图片"""
|
||||
logger.info(f"开始加载图片: {self.url}")
|
||||
def _initImageLoading(self):
|
||||
"""初始化图片加载,优先使用getFileUrl方法获取临时URL"""
|
||||
if self.fileId:
|
||||
logger.info(f"使用文件URI {self.fileId} 获取临时预览URL")
|
||||
try:
|
||||
# 使用getFileUrl方法获取临时URL
|
||||
from app.core.api import miaoStarsBasicApi
|
||||
response = miaoStarsBasicApi.getFileUrl(self.fileId, redirect=False)
|
||||
|
||||
# 详细记录响应内容
|
||||
logger.debug(f"getFileUrl响应: {response}")
|
||||
|
||||
# 处理getFileUrl的返回值,支持多种格式
|
||||
if isinstance(response, str):
|
||||
# 直接返回URL字符串的情况
|
||||
preview_url = response.strip('` ')
|
||||
logger.info(f"成功获取临时预览URL: {preview_url}")
|
||||
self.url = preview_url
|
||||
elif isinstance(response, dict):
|
||||
# 检查是否有code字段表示错误
|
||||
if response.get('code') == -1:
|
||||
error_msg = response.get('msg', '获取临时URL失败')
|
||||
logger.warning(f"获取临时URL失败: {error_msg},将使用原始URL作为fallback")
|
||||
# 检查是否有urls字段(Cloudreve API返回格式)
|
||||
elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0:
|
||||
preview_url = response['urls'][0].get('url', '').strip('` ')
|
||||
logger.info(f"成功获取临时预览URL: {preview_url}")
|
||||
self.url = preview_url
|
||||
# 检查是否有data字段(Cloudreve V4 API标准格式)
|
||||
elif 'data' in response:
|
||||
data = response['data']
|
||||
if isinstance(data, dict):
|
||||
# 如果data是字典,检查是否包含urls数组
|
||||
if 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0:
|
||||
preview_url = data['urls'][0].get('url', '').strip('` ')
|
||||
logger.info(f"成功从data中获取临时预览URL: {preview_url}")
|
||||
self.url = preview_url
|
||||
else:
|
||||
# 尝试将data直接转为字符串作为fallback
|
||||
preview_url = str(data).strip('` ')
|
||||
logger.warning(f"data字段不包含urls数组,使用原始data字符串: {preview_url}")
|
||||
self.url = preview_url
|
||||
else:
|
||||
# data不是字典,直接转为字符串
|
||||
preview_url = str(data).strip('` ')
|
||||
logger.info(f"成功获取临时预览URL: {preview_url}")
|
||||
self.url = preview_url
|
||||
else:
|
||||
error_msg = response.get('msg', '获取临时URL失败')
|
||||
logger.warning(f"获取临时URL失败: {error_msg},将使用原始URL作为fallback")
|
||||
if self.url:
|
||||
self.url = self._ensure_full_url(self.url)
|
||||
logger.info(f"使用处理后的fallback URL: {self.url}")
|
||||
else:
|
||||
logger.error("没有可用的URL,无法加载图片")
|
||||
self.handleError("没有可用的图片URL")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"获取预览URL时发生异常: {str(e)}")
|
||||
# 尝试使用备用方式处理
|
||||
if self.url:
|
||||
self.url = self._ensure_full_url(self.url)
|
||||
logger.info(f"异常后使用处理后的fallback URL: {self.url}")
|
||||
else:
|
||||
logger.error("异常后没有可用的URL,无法加载图片")
|
||||
self.handleError(f"获取URL异常: {str(e)}")
|
||||
return
|
||||
else:
|
||||
# 如果没有fileId,尝试处理原始URL
|
||||
if self.url:
|
||||
self.url = self._ensure_full_url(self.url)
|
||||
logger.info(f"使用处理后的原始URL: {self.url}")
|
||||
else:
|
||||
logger.error("没有提供文件ID或URL,无法加载图片")
|
||||
self.handleError("没有提供图片URL")
|
||||
return
|
||||
|
||||
# 创建图片加载线程
|
||||
self.imageLoaderThread = ImageLoaderThread(self.url)
|
||||
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
|
||||
self.imageLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
|
||||
# 开始加载
|
||||
self.imageLoaderThread.start()
|
||||
logger.info(f"开始加载图片: {self.url}")
|
||||
|
||||
# startLoading方法已被_initImageLoading替代
|
||||
|
||||
def updateProgress(self, progress):
|
||||
"""更新加载进度"""
|
||||
@@ -204,8 +281,8 @@ class PreviewTextBox(MessageBoxBase):
|
||||
self.loadingCard.load()
|
||||
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 使用文本加载线程
|
||||
self.textLoaderThread = TextLoaderThread(url)
|
||||
# 确保在创建线程前使用处理后的URL
|
||||
self.textLoaderThread = TextLoaderThread(self.url, fileId=_id)
|
||||
self.textLoaderThread.textLoaded.connect(self.setTextContent)
|
||||
self.textLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
@@ -311,6 +388,41 @@ class PreviewTextBox(MessageBoxBase):
|
||||
|
||||
def startLoading(self):
|
||||
"""开始加载文本"""
|
||||
# 如果有fileId,尝试获取临时URL
|
||||
if self._id:
|
||||
logger.info(f"使用文件URI {self._id} 获取临时文本URL")
|
||||
response = miaoStarsBasicApi.getFileUrl(self._id, redirect=False)
|
||||
|
||||
# 处理getFileUrl的返回值,支持多种格式
|
||||
preview_url = None
|
||||
if isinstance(response, dict):
|
||||
# 检查是否是Cloudreve V4 API格式 (data.urls)
|
||||
if response.get('code') == 0 and 'data' in response:
|
||||
data = response['data']
|
||||
# 格式1: data.urls数组
|
||||
if isinstance(data, dict) and 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0:
|
||||
preview_url = data['urls'][0].get('url', '').strip('` ')
|
||||
# 格式2: data直接包含URL
|
||||
elif isinstance(data, str):
|
||||
preview_url = data.strip('` ')
|
||||
# 检查是否直接包含urls字段
|
||||
elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0:
|
||||
preview_url = response['urls'][0].get('url', '').strip('` ')
|
||||
|
||||
# 如果成功获取到URL
|
||||
if preview_url:
|
||||
self.url = preview_url
|
||||
logger.info(f"成功获取临时文本URL: {self.url}")
|
||||
else:
|
||||
error_msg = response.get('msg', '获取临时URL失败')
|
||||
logger.warning(f"获取临时URL失败: {error_msg},将使用处理后的原始URL")
|
||||
|
||||
# 更新线程的URL和fileId
|
||||
self.textLoaderThread = TextLoaderThread(self.url, fileId=self._id)
|
||||
self.textLoaderThread.textLoaded.connect(self.setTextContent)
|
||||
self.textLoaderThread.errorOccurred.connect(self.handleError)
|
||||
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
|
||||
|
||||
logger.info(f"开始加载文本文件: {self.url}")
|
||||
self.textLoaderThread.start()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user