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
miaoStarsBasicApi = MiaoStarsBasicApi()

View File

@@ -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"获取文件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):
"""更新用户昵称 (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()

View File

@@ -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", "上传失败")

View File

@@ -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)

View File

@@ -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

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/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>

View 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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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()