fix lot of thing

This commit is contained in:
2025-11-01 20:14:35 +08:00
parent 39dfb62cbf
commit f006729311
16 changed files with 23303 additions and 3996 deletions

View File

@@ -1,4 +1,4 @@
from .basicApi import MiaoStarsBasicApi from .basicApi import MiaoStarsBasicApi, basicApi
from ..utils.config import userConfig from ..utils.config import userConfig
miaoStarsBasicApi = MiaoStarsBasicApi() miaoStarsBasicApi = MiaoStarsBasicApi()

View File

@@ -371,14 +371,16 @@ class MiaoStarsBasicApi:
if path == "/" or path == "": if path == "/" or path == "":
uri = "cloudreve://my/" uri = "cloudreve://my/"
else: else:
# 清理路径确保不包含cloudreve://my前缀
normalized_path = path.strip("/").replace("\\", "/").replace("cloudreve://my/", "")
# 使用quote_plus正确编码路径确保特殊字符被正确处理 # 使用quote_plus正确编码路径确保特殊字符被正确处理
# 首先规范化路径分隔符
normalized_path = path.strip("/").replace("\\", "/")
# 对路径部分进行URL编码
encoded_path = quote_plus(normalized_path) encoded_path = quote_plus(normalized_path)
# 由于quote_plus会将斜杠也编码我们需要恢复它们 # 由于quote_plus会将斜杠也编码我们需要恢复它们
encoded_path = encoded_path.replace("%2F", "/") encoded_path = encoded_path.replace("%2F", "/")
uri = f"cloudreve://my/{encoded_path}" uri = f"cloudreve://my/{encoded_path}"
# 确保路径格式正确,移除重复的前缀
uri = uri.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
# 添加必需的分页参数 # 添加必需的分页参数
params = { params = {
@@ -403,12 +405,12 @@ class MiaoStarsBasicApi:
def getPolicy(self): def getPolicy(self):
"""获取用户存储策略""" """获取用户存储策略"""
url = "/user/policies" url = "/user/setting/policies"
r = self.request("GET", url) r = self.request("GET", url)
# 转换响应格式 # 转换响应格式
if isinstance(r, list): if isinstance(r, list):
return {"code": 0, "data": r} return r
else: else:
return {"code": 0, "data": []} return {"code": 0, "data": []}
@@ -427,11 +429,28 @@ class MiaoStarsBasicApi:
"""创建文件夹""" """创建文件夹"""
url = "/file/create" url = "/file/create"
currentPath = policyConfig.returnCurrentPath() currentPath = policyConfig.returnCurrentPath()
# 根据Cloudreve V4 API规范构建uri参数确保不会有重复斜杠 # 根据Cloudreve V4 API规范构建uri参数确保不会有重复斜杠和前缀
if currentPath == "/": # 清理currentPath确保不包含cloudreve://my前缀
clean_current_path = currentPath.replace("cloudreve://my", "")
if clean_current_path == "/":
uri = f"cloudreve://my/{name}" uri = f"cloudreve://my/{name}"
else: 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}) r = self.request("POST", url, json={"uri": uri, "type": "folder", "err_on_conflict": True})
# 转换响应格式 # 转换响应格式
@@ -440,20 +459,26 @@ class MiaoStarsBasicApi:
else: else:
return {"code": -1, "msg": r.get("msg", "文件夹创建失败")} return {"code": -1, "msg": r.get("msg", "文件夹创建失败")}
def deleteFile(self, fileId, fileType: Literal["file", "dir"]): def deleteFile(self, fileUri, fileType: Literal["file", "dir"]):
"""删除文件""" """删除文件 (Cloudreve V4 API)"""
url = "/file/delete" url = "/file"
# 现在调用方已经传入了正确格式的URI
logger.debug(f"删除文件URI: {fileUri}")
# 根据Cloudreve V4 API规范使用uris参数列表
deleteData = { deleteData = {
"items": [fileId] if fileType == "file" else [], "uris": [fileUri]
"dirs": [fileId] if fileType == "dir" else []
} }
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": "删除成功"} return {"code": 0, "msg": "删除成功"}
else: else:
return {"code": -1, "msg": r.get("error", "删除失败")} error_msg = r.get("msg", r.get("error", "删除失败"))
return {"code": -1, "msg": error_msg}
def wareSearch( def wareSearch(
self, self,
@@ -564,6 +589,213 @@ class MiaoStarsBasicApi:
else: else:
return {"code": -1, "msg": r.get("error", "文件内容更新失败")} 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"获取文件URLURI: {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): def updateUserNickname(self, nickName):
"""更新用户昵称 (Cloudreve V4 API)""" """更新用户昵称 (Cloudreve V4 API)"""
url = "/user/profile" url = "/user/profile"
@@ -577,3 +809,7 @@ class MiaoStarsBasicApi:
return {"code": 0, "msg": "昵称更新成功"} return {"code": 0, "msg": "昵称更新成功"}
else: else:
return {"code": -1, "msg": r.get("error", "昵称更新失败")} return {"code": -1, "msg": r.get("error", "昵称更新失败")}
# 创建全局实例,方便其他模块直接使用
basicApi = MiaoStarsBasicApi()

View File

@@ -140,22 +140,22 @@ class DeleteFileThread(QThread):
successDelete = pyqtSignal() successDelete = pyqtSignal()
errorDelete = pyqtSignal(str) errorDelete = pyqtSignal(str)
def __init__(self, fileId: str, fileType: str): def __init__(self, fileUri: str, fileType: str):
super().__init__() super().__init__()
logger.debug(f"初始化删除文件线程 - ID: {fileId}, 类型: {fileType}") logger.debug(f"初始化删除文件线程 - URI: {fileUri}, 类型: {fileType}")
self.fileId = fileId self.fileUri = fileUri
self.fileType = fileType self.fileType = fileType
def run(self): def run(self):
logger.info(f"开始删除文件 - ID: {self.fileId}, 类型: {self.fileType}") logger.info(f"开始删除文件 - URI: {self.fileUri}, 类型: {self.fileType}")
try: try:
response = miaoStarsBasicApi.deleteFile(self.fileId, self.fileType) response = miaoStarsBasicApi.deleteFile(self.fileUri, self.fileType)
if response["code"] == 0: if response["code"] == 0:
logger.info(f"文件删除成功 - ID: {self.fileId}") logger.info(f"文件删除成功 - URI: {self.fileUri}")
self.successDelete.emit() self.successDelete.emit()
else: else:
logger.error( logger.error(
f"文件删除失败 - ID: {self.fileId}, 错误: {response.get('msg', '未知错误')}" f"文件删除失败 - URI: {self.fileUri}, 错误: {response.get('msg', '未知错误')}"
) )
self.errorDelete.emit(f"删除失败: {response.get('msg', '未知错误')}") self.errorDelete.emit(f"删除失败: {response.get('msg', '未知错误')}")
@@ -237,7 +237,11 @@ class UploadThread(QThread):
self.applicationUrl = "/file/upload" self.applicationUrl = "/file/upload"
self.current_path = policyConfig.returnCurrentPath() 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 = { 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", "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" return mime_type if mime_type else "application/octet-stream"
def _prepareUploadData(self) -> Dict[str, Any]: def _prepareUploadData(self) -> Dict[str, Any]:
"""准备上传数据""" """准备上传数据符合Cloudreve V4 API规范"""
try: try:
modification_time = os.path.getmtime(self.file_path) modification_time = os.path.getmtime(self.file_path)
size = os.path.getsize(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 { return {
"path": self.current_path, "uri": uri,
"size": size, "size": size,
"name": self.file_name,
"policy_id": self.policy, "policy_id": self.policy,
"last_modified": int(modification_time * 1000), # 转换为毫秒 "last_modified": int(modification_time * 1000), # 转换为毫秒
"mime_type": self.getMimeType(self.file_path), "mime_type": self.getMimeType(self.file_path),
} }
except (OSError, ValueError) as e: except (OSError, ValueError) as e:
logger.error(f"准备请求失败: {e}") logger.error(f"准备请求失败: {e}")
raise
def _uploadWithProgress(self, upload_url: str, credential: str, total_size: int): def _uploadWithProgress(self, upload_url: str, credential: str, total_size: int):
"""带进度显示的上传方法""" """带进度显示的上传方法"""
try: 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") self._file_obj = open(self.file_path, "rb")
logger.info("文件打开成功")
# 准备上传头信息 # 准备上传头信息
upload_headers = { 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", "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": "*/*",
"Authorization": credential,
"Accept-Language": "zh-CN,zh;q=0.9", "Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Encoding": "gzip, deflate, br, zstd",
"Content-Type": self.getMimeType(self.file_path), # 使用正确的MIME类型 "Content-Type": self.getMimeType(self.file_path), # 使用正确的MIME类型
"Content-Length": str(total_size), # 添加必需的Content-Length头
# 删除硬编码的Origin和Referer使用miaoStarsBasicApi中已配置的请求头 # 删除硬编码的Origin和Referer使用miaoStarsBasicApi中已配置的请求头
"Sec-Fetch-Dest": "empty", "Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site", "Sec-Fetch-Site": "cross-site",
**auth_header, # 只在credential不为None时添加Authorization头
} }
logger.info(f"上传头信息准备完成: {upload_headers}")
# 发送预检请求 # 尝试发送预检请求,但不阻塞主流程
options_result = miaoStarsBasicApi.returnSession().options( try:
upload_url, headers=upload_headers logger.info("尝试发送OPTIONS预检请求")
) options_result = miaoStarsBasicApi.returnSession().options(
options_result.raise_for_status() upload_url, headers=upload_headers, timeout=10
logger.info("OPTIONS预检请求完成") )
options_result.raise_for_status()
logger.info("OPTIONS预检请求完成")
except Exception as e:
logger.warning(f"OPTIONS预检请求失败但继续上传: {str(e)}")
# 创建自定义请求体以支持进度监控和取消 # 创建自定义请求体以支持进度监控和取消
class ProgressFileObject: class ProgressFileObject:
def __init__( def __init__(
self, file_obj, total_size, progress_callback, cancel_check self, file_obj, total_size, progress_callback, cancel_check
): ):
logger.info("创建ProgressFileObject对象")
self.file_obj = file_obj self.file_obj = file_obj
self.total_size = total_size self.total_size = total_size
self.progress_callback = progress_callback self.progress_callback = progress_callback
@@ -319,7 +368,8 @@ class UploadThread(QThread):
def read(self, size=-1): def read(self, size=-1):
# 检查是否取消 # 检查是否取消
if self.cancel_check(): if self.cancel_check():
return logger.info("检测到取消上传请求")
raise InterruptedError("Upload cancelled by user")
data = self.file_obj.read(size) data = self.file_obj.read(size)
if data: if data:
@@ -330,12 +380,14 @@ class UploadThread(QThread):
else 0 else 0
) )
self.progress_callback(progress, self.uploaded, self.total_size) self.progress_callback(progress, self.uploaded, self.total_size)
logger.debug(f"上传进度: {progress:.2f}% ({self.uploaded}/{self.total_size} 字节)")
return data return data
def __len__(self): def __len__(self):
return self.total_size return self.total_size
# 创建带进度监控的文件对象 # 创建带进度监控的文件对象
logger.info("创建进度监控文件对象")
progress_file = ProgressFileObject( progress_file = ProgressFileObject(
self._file_obj, self._file_obj,
total_size, total_size,
@@ -347,17 +399,60 @@ class UploadThread(QThread):
# 执行实际上传 # 执行实际上传
logger.info(f"开始上传文件,总大小: {total_size} 字节") logger.info(f"开始上传文件,总大小: {total_size} 字节")
upload_result = miaoStarsBasicApi.returnSession().post( logger.info(f"上传URL: {upload_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 }}")
data=progress_file,
headers=upload_headers, # 修复优化上传逻辑先尝试PUT方法Cloudreve V4本地存储通常使用PUT
timeout=60, try:
) # 首先尝试使用PUT方法
logger.info("尝试使用PUT方法上传")
logger.info(f"上传完成,响应状态: {upload_result.status_code}") upload_result = miaoStarsBasicApi.returnSession().put(
upload_result.raise_for_status() upload_url,
data=progress_file,
return upload_result 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
except Exception as e: except Exception as e:
logger.error(f"上传过程中发生错误: {e}") logger.error(f"上传过程中发生错误: {e}")
@@ -379,6 +474,24 @@ class UploadThread(QThread):
# 准备上传数据 # 准备上传数据
upload_data = self._prepareUploadData() 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: if self._is_cancelled:
@@ -387,41 +500,126 @@ class UploadThread(QThread):
# 执行上传请求获取上传URL # 执行上传请求获取上传URL
logger.info("请求上传URL") logger.info("请求上传URL")
# 从miaoStarsBasicApi获取完整URL(去掉/api/v4后缀 # 直接使用basicApi构建完整URL,保留/api/v4后缀
base_url = miaoStarsBasicApi.basicApi.rstrip('/api/v4') # 使用API类中定义的基础URL # Cloudreve V4 API要求上传接口路径为/api/v4/file/upload
full_url = f"{base_url}{self.applicationUrl}" full_url = f"{miaoStarsBasicApi.basicApi}{self.applicationUrl}"
logger.info(f"构建的上传请求URL: {full_url}")
logger.info(f"上传请求数据: {upload_data}")
response = miaoStarsBasicApi.returnSession().put( response = miaoStarsBasicApi.returnSession().put(
full_url, json=upload_data, headers=self.headers, timeout=30 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() 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: if result.get("code") == 0:
self.uploadApplicationApprovedSignal.emit() self.uploadApplicationApprovedSignal.emit()
upload_urls = result.get("data").get("uploadURLs") data = result.get("data", {})
credential = result.get("data").get("credential") 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}")
self.uploadUrl = upload_urls[0] + "?chunk=0" # 优先检查是否有session_id因为这是必须的
logger.info("获取到上传URL") 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) total_size = os.path.getsize(self.file_path)
# 执行带进度显示的上传 # 执行带进度显示的上传
upload_result = self._uploadWithProgress( logger.info("开始调用_uploadWithProgress方法执行上传")
self.uploadUrl, credential, total_size try:
) upload_result = self._uploadWithProgress(
self.uploadUrl, credential, total_size
)
logger.info("_uploadWithProgress方法调用完成")
# 检查是否取消 # 检查是否取消
if self._is_cancelled: if self._is_cancelled:
self.uploadCancelled.emit() 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 return
# 处理上传结果 try:
logger.info(f"上传响应: {upload_result}") # 解析上传响应
self.uploadFinished.emit() 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: else:
error_msg = result.get("msg", "上传失败") error_msg = result.get("msg", "上传失败")

View File

@@ -1,11 +1,14 @@
import os import os
import logging
from urllib.parse import urlparse from urllib.parse import urlparse
import requests import requests
from PyQt6.QtCore import QThread, pyqtSignal from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap 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): class TextLoaderThread(QThread):
@@ -15,54 +18,88 @@ class TextLoaderThread(QThread):
errorOccurred = pyqtSignal(str) errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号 progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(self, url): def __init__(self, url, fileId=None):
super().__init__() super().__init__()
self.url = url self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
def run(self): def run(self):
"""线程执行函数""" """线程执行函数"""
try: try:
# 1. 设置网络请求参数 - 优化连接参数 binary_content = None
session = requests.Session() content_chunks = [] # 预先初始化content_chunks避免在任何执行路径下出现未定义错误
adapter = requests.adapters.HTTPAdapter(
pool_connections=20, # 如果提供了fileId实际上是URI优先使用getFileContent直接获取内容
pool_maxsize=20, if self.fileId:
max_retries=5, # 增加重试次数 self.progressUpdated.emit(0) # 发送初始进度
pool_block=False, logger.info(f"使用文件URI {self.fileId} 直接获取文件内容")
) response = basicApi.getFileContent(self.fileId)
session.mount("http://", adapter)
session.mount("https://", adapter) 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")
# 如果直接获取内容失败尝试使用URL
if binary_content is None:
if not self.url:
self.errorOccurred.emit("没有可用的URL来加载文本内容")
return
# 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)
# 2. 增加超时时间并添加重试机制 # 2. 增加超时时间并添加重试机制
response = miaoStarsBasicApi.returnSession().get( response = basicApi.returnSession().get(
self.url, self.url,
stream=True, stream=True,
timeout=(15, 30), # 增加超时时间连接15秒读取30秒 timeout=(15, 30), # 增加超时时间连接15秒读取30秒
) )
response.raise_for_status() response.raise_for_status()
# 3. 获取文件大小(如果服务器支持) # 3. 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0)) total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0 downloaded_size = 0
# 4. 分块读取并处理 - 使用二进制读取提高速度 # 4. 分块读取并处理 - 使用二进制读取提高速度
content_chunks = [] content_chunks = []
for chunk in response.iter_content(chunk_size=16384): # 增大块大小 for chunk in response.iter_content(chunk_size=16384): # 增大块大小
if chunk: if chunk:
content_chunks.append(chunk) content_chunks.append(chunk)
downloaded_size += len(chunk) downloaded_size += len(chunk)
# 更新进度(如果知道总大小) # 更新进度(如果知道总大小)
if total_size > 0: if total_size > 0:
progress = int((downloaded_size / total_size) * 100) progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress) self.progressUpdated.emit(progress)
# 5. 合并内容并解码 # 5. 合并内容 - 使用正确的二进制合并语法
binary_content = b"".join(content_chunks) binary_content = b"" .join(content_chunks)
if not binary_content: # 确保binary_content是bytes类型且不为空
self.errorOccurred.emit("下载内容为空") 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 return
# 6. 智能编码检测和解码 # 6. 智能编码检测和解码
@@ -113,10 +150,11 @@ class ImageLoaderThread(QThread):
progressUpdated = pyqtSignal(int) # 进度更新信号 progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__( 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缓存 ): # 50MB缓存
super().__init__() super().__init__()
self.url = url self.url = url # 原始URL可能作为fallback
self.fileId = fileId # 文件ID优先使用
self.cache_dir = cache_dir self.cache_dir = cache_dir
self.max_cache_size = max_cache_size self.max_cache_size = max_cache_size
self._setup_cache() self._setup_cache()
@@ -184,48 +222,108 @@ class ImageLoaderThread(QThread):
def run(self): def run(self):
"""线程执行函数""" """线程执行函数"""
try: try:
# 1. 首先检查缓存 image_data = None
cached_pixmap = self._get_cached_image()
if cached_pixmap: # 如果提供了fileId实际上是URI优先使用getFileContent直接获取内容
self.imageLoaded.emit(cached_pixmap) if self.fileId:
return logger.info(f"使用文件URI {self.fileId} 直接获取图片内容")
response = miaoStarsBasicApi.getFileContent(self.fileId)
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")
# 如果直接获取内容失败尝试使用URL
if image_data is None:
if not self.url:
self.errorOccurred.emit("没有可用的URL来加载图片内容")
return
# 确保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
# 1. 首先检查缓存
cached_pixmap = self._get_cached_image()
if cached_pixmap:
self.imageLoaded.emit(cached_pixmap)
return
# 2. 设置更短的超时时间 # 2. 设置更短的超时时间
session = requests.Session() session = requests.Session()
adapter = requests.adapters.HTTPAdapter( adapter = requests.adapters.HTTPAdapter(
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次 pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
) )
session.mount("http://", adapter) session.mount("http://", adapter)
session.mount("https://", adapter) session.mount("https://", adapter)
# 3. 流式下载,支持进度显示 # 3. 流式下载,支持进度显示
response = miaoStarsBasicApi.returnSession().get( response = miaoStarsBasicApi.returnSession().get(
self.url, stream=True, timeout=(20, 30) # 连接超时5秒读取超时10秒 self.url, stream=True, timeout=(20, 30) # 连接超时5秒读取超时10秒
) )
response.raise_for_status() response.raise_for_status()
# 获取文件大小(如果服务器支持) # 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0)) total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0 downloaded_size = 0
# 4. 分块读取并处理 # 4. 分块读取并处理
image_data = b"" image_data = b""
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
if chunk: if chunk:
image_data += chunk image_data += chunk
downloaded_size += len(chunk) downloaded_size += len(chunk)
# 更新进度(如果知道总大小) # 更新进度(如果知道总大小)
if total_size > 0: if total_size > 0:
progress = int((downloaded_size / total_size) * 100) # 确保进度不超过100%
self.progressUpdated.emit(progress) progress = min(int((downloaded_size / total_size) * 100), 100)
self.progressUpdated.emit(progress)
# 5. 从数据创建QImage比QPixmap更快 # 5. 从数据创建QImage比QPixmap更快
image = QImage() image = QImage()
image.loadFromData(image_data)
# 检查数据是否为空或类型是否正确
if image.isNull(): if not image_data:
raise Exception("无法加载图片数据") 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 # 6. 转换为QPixmap
pixmap = QPixmap.fromImage(image) pixmap = QPixmap.fromImage(image)

View File

@@ -3,4 +3,4 @@
Version information for the application 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<file>images/load.png</file> <file>images/load.png</file>
<file>images/error.png</file> <file>images/error.png</file>
<file>images/empty.png</file> <file>images/empty.png</file>
<file>images/title.jpg</file> <file>images/title.png</file>
<file>images/loadFailure.png</file> <file>images/loadFailure.png</file>
<file>images/logolong.png</file> <file>images/logolong.png</file>

View File

@@ -38,7 +38,7 @@ class AppInfoInterface(QWidget):
mainLayout.setContentsMargins(30, 30, 30, 30) mainLayout.setContentsMargins(30, 30, 30, 30)
mainLayout.setAlignment(Qt.AlignmentFlag.AlignTop) 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) self.titleImageLabel.scaledToHeight(130)
mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter) mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter)

View File

@@ -95,14 +95,26 @@ class FileCard(CardWidget):
full_path = f"cloudreve://my/{self.fileName}" full_path = f"cloudreve://my/{self.fileName}"
else: else:
# 子目录情况,确保正确拼接路径 # 子目录情况,确保正确拼接路径
# 清理路径,避免重复的斜杠 # 清理路径,避免重复的斜杠和前缀
clean_path = self.filePath.lstrip("/") 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 = f"cloudreve://my/{clean_path}/{self.fileName}"
# 确保路径格式正确,没有重复的部分
# 确保路径格式正确,移除重复的前缀
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my") 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( signalBus.addDownloadFileTask.emit(
f"own.{self.suffix}", self.fileName, full_path f"own.{self.suffix}", self.fileName, full_path
@@ -158,7 +170,34 @@ class FileCard(CardWidget):
parent=self.window(), parent=self.window(),
) )
if w.exec(): 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.successDelete.connect(self.deleteSuccess)
self.deleteThread.errorDelete.connect(self.deleteError) self.deleteThread.errorDelete.connect(self.deleteError)
self.deleteThread.start() self.deleteThread.start()
@@ -200,6 +239,16 @@ class FileCard(CardWidget):
def contextMenuEvent(self, e): def contextMenuEvent(self, e):
"""重写上下文菜单事件,确保只有右键点击才会触发""" """重写上下文菜单事件,确保只有右键点击才会触发"""
pass pass
def mouseDoubleClickEvent(self, event):
"""重写鼠标双击事件,双击文件夹时进入文件夹"""
if self.fileType == "dir":
self.dirClicked()
# 阻止事件继续传播,避免可能的干扰
event.accept()
else:
# 非文件夹时调用父类处理
super().mouseDoubleClickEvent(event)
class ShareFileCard(CardWidget): class ShareFileCard(CardWidget):
@@ -360,3 +409,13 @@ class SharedFolderFileCard(CardWidget):
) )
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN) menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def mouseDoubleClickEvent(self, event):
"""重写鼠标双击事件,双击文件夹时进入文件夹"""
if self.fileType == "dir":
self.dirClicked()
# 阻止事件继续传播,避免可能的干扰
event.accept()
else:
# 非文件夹时调用父类处理
super().mouseDoubleClickEvent(event)

View File

@@ -68,8 +68,37 @@ class LinkageSwitchingBase(ScrollArea):
fileDate = data.get("created_at", data.get("date", "")) fileDate = data.get("created_at", data.get("date", ""))
fileSize = data.get("size", 0) 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( fileCard = FileCard(
fileId, full_uri, # 传递完整的URI而不是文件ID
fileName, fileName,
fileType, fileType,
filePath, filePath,

View File

@@ -181,16 +181,15 @@ class MainWindow(CustomFluentWindow):
# 窗口大小改变时更新背景 # 窗口大小改变时更新背景
def imagePreview(self, _id): def imagePreview(self, _id):
# 使用V4 API进行预览 # 直接传递文件ID给OptimizedPreviewBox让它内部使用getFileUrl获取临时URL
url = f"/file/preview/{_id}" self.previewBox = OptimizedPreviewBox(self, fileId=_id)
self.previewBox = OptimizedPreviewBox(self, url)
if self.previewBox.exec(): if self.previewBox.exec():
pass pass
def txtPreview(self, _id): def txtPreview(self, _id):
# 使用V4 API获取内容 # 直接使用fileUri不再构建URL
url = f"/file/content/{_id}" # _id 应该是 cloudreve:// 格式的URI
self.previewBox = PreviewTextBox(self, url, _id) self.previewBox = PreviewTextBox(self, "", _id)
if self.previewBox.exec(): if self.previewBox.exec():
pass pass

View File

@@ -37,11 +37,11 @@ def createThumbnail(pixmap, max_size=200):
class OptimizedPreviewBox(MessageBoxBase): class OptimizedPreviewBox(MessageBoxBase):
def __init__(self, parent=None, url=None): def __init__(self, parent=None, url=None, fileId=None):
super().__init__(parent=parent) super().__init__(parent=parent)
# 处理URL确保它是完整的URL self.fileId = fileId # 保存文件ID
self.url = self._ensure_full_url(url) self.url = url # 暂存原始URL可能需要作为fallback
logger.info(f"初始化图片预览框URL: {self.url}") logger.info(f"初始化图片预览框,文件ID: {self.fileId}, 原始URL: {self.url}")
self.widget.setMinimumSize(500, 500) self.widget.setMinimumSize(500, 500)
self.original_pixmap = None self.original_pixmap = None
@@ -58,16 +58,9 @@ class OptimizedPreviewBox(MessageBoxBase):
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放 self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter) 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初始化 # 延迟启动加载避免阻塞UI初始化
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
QTimer.singleShot(100, self._initImageLoading)
QTimer.singleShot(100, self.startLoading)
def _ensure_full_url(self, url): def _ensure_full_url(self, url):
"""确保URL是完整的添加scheme和base URL如果缺失""" """确保URL是完整的添加scheme和base URL如果缺失"""
@@ -87,10 +80,94 @@ class OptimizedPreviewBox(MessageBoxBase):
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}") logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
return full_url return full_url
def startLoading(self): def _initImageLoading(self):
"""开始加载图片""" """初始化图片加载优先使用getFileUrl方法获取临时URL"""
logger.info(f"开始加载图片: {self.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() self.imageLoaderThread.start()
logger.info(f"开始加载图片: {self.url}")
# startLoading方法已被_initImageLoading替代
def updateProgress(self, progress): def updateProgress(self, progress):
"""更新加载进度""" """更新加载进度"""
@@ -204,8 +281,8 @@ class PreviewTextBox(MessageBoxBase):
self.loadingCard.load() self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter) self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 使用文本加载线程 # 确保在创建线程前使用处理后的URL
self.textLoaderThread = TextLoaderThread(url) self.textLoaderThread = TextLoaderThread(self.url, fileId=_id)
self.textLoaderThread.textLoaded.connect(self.setTextContent) self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError) self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress) self.textLoaderThread.progressUpdated.connect(self.updateProgress)
@@ -311,6 +388,41 @@ class PreviewTextBox(MessageBoxBase):
def startLoading(self): 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}") logger.info(f"开始加载文本文件: {self.url}")
self.textLoaderThread.start() self.textLoaderThread.start()