Files
cloudnote/main_pyqt5.py
T
2026-06-01 17:39:33 +08:00

2533 lines
106 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
云笔记应用程序
支持本地和云端文件管理,支持文本文件(.txt)和加密笔记(.bjb)
完整修复版 - 支持端到端加密,云端下载的单独密钥文件需用户提供.key文件
"""
import sys
import os
import json
import tempfile
import traceback
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import requests
# 导入加密模块
from DVT_RFSA import *
# ==================== 调试日志配置 ====================
DEBUG = True # True=记录日志, False=不记录日志
def debug_log(msg, level="INFO"):
"""调试日志函数"""
if DEBUG:
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[{timestamp}] [{level}] {msg}")
sys.stdout.flush()
def debug_error(msg):
"""错误日志(带堆栈)"""
if DEBUG:
debug_log(msg, "ERROR")
traceback.print_exc()
# ==================== 配置 ====================
API_BASE_URL = "https://noteapi.dvssvc.site/api"
APP_NAME = "CloudNote"
def get_app_dir():
"""获取程序所在目录"""
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(__file__))
APP_DIR = get_app_dir()
USER_KEY_DIR = os.path.expanduser(f"~/.{APP_NAME.lower()}/keys")
APP_KEY_DIR = os.path.join(APP_DIR, ".keys")
if DEBUG:
debug_log(f"程序目录: {APP_DIR}")
debug_log(f"用户密钥目录: {USER_KEY_DIR}")
debug_log(f"程序密钥目录: {APP_KEY_DIR}")
KEY_FILES = {
'rsa_private': "global_rsa_private.pem",
'rsa_public': "global_rsa_public.pem",
'x25519_private': "global_x25519_private.bin",
'x25519_public': "global_x25519_public.bin"
}
def get_key_path(key_name, prefer_user=True):
"""获取密钥文件路径,优先使用用户目录"""
if prefer_user:
search_dirs = [USER_KEY_DIR, APP_KEY_DIR]
dir_names = ['user', 'app']
else:
search_dirs = [APP_KEY_DIR, USER_KEY_DIR]
dir_names = ['app', 'user']
for dir_path, dir_type in zip(search_dirs, dir_names):
file_path = os.path.join(dir_path, key_name)
if os.path.exists(file_path):
if DEBUG:
debug_log(f"找到密钥 {key_name} 在 {dir_type} 目录: {file_path}")
return file_path, dir_type
if DEBUG:
debug_log(f"密钥 {key_name} 未找到,将使用用户目录路径")
return os.path.join(USER_KEY_DIR, key_name), 'user'
def ensure_directory(dir_path):
"""确保目录存在"""
try:
os.makedirs(dir_path, exist_ok=True)
if DEBUG:
debug_log(f"目录已确保: {dir_path}")
return True
except Exception as e:
debug_error(f"创建目录失败 {dir_path}: {e}")
return False
# ==================== 全局密钥管理(双路径) ====================
def ensure_global_keys():
"""确保全局密钥对存在,支持双路径存储"""
if DEBUG:
debug_log("开始检查/生成全局密钥(双路径模式)...")
ensure_directory(USER_KEY_DIR)
ensure_directory(APP_KEY_DIR)
key_pairs = [
('rsa', KEY_FILES['rsa_private'], KEY_FILES['rsa_public'], generate_rsa_key, 4096),
('x25519', KEY_FILES['x25519_private'], KEY_FILES['x25519_public'], generate_x25519_keys, None)
]
for key_type, priv_file, pub_file, gen_func, key_size in key_pairs:
user_priv = os.path.join(USER_KEY_DIR, priv_file)
user_pub = os.path.join(USER_KEY_DIR, pub_file)
user_exists = os.path.exists(user_priv) and os.path.exists(user_pub)
app_priv = os.path.join(APP_KEY_DIR, priv_file)
app_pub = os.path.join(APP_KEY_DIR, pub_file)
app_exists = os.path.exists(app_priv) and os.path.exists(app_pub)
if DEBUG:
debug_log(f"{key_type.upper()}密钥对 - 用户目录存在: {user_exists}, 程序目录存在: {app_exists}")
if not user_exists:
if DEBUG:
debug_log(f"开始生成{key_type.upper()}密钥对到用户目录...")
try:
if key_size:
priv, pub = gen_func(key_size)
else:
priv, pub = gen_func()
with open(user_priv, 'wb') as f:
f.write(priv)
with open(user_pub, 'wb') as f:
f.write(pub)
try:
with open(app_priv, 'wb') as f:
f.write(priv)
with open(app_pub, 'wb') as f:
f.write(pub)
if DEBUG:
debug_log(f"{key_type.upper()}密钥备份到程序目录成功")
except Exception as e:
if DEBUG:
debug_log(f"备份到程序目录失败: {e}")
except Exception as e:
debug_error(f"{key_type.upper()}密钥生成失败: {e}")
raise
elif user_exists and not app_exists:
try:
with open(user_priv, 'rb') as f:
priv = f.read()
with open(user_pub, 'rb') as f:
pub = f.read()
with open(app_priv, 'wb') as f:
f.write(priv)
with open(app_pub, 'wb') as f:
f.write(pub)
if DEBUG:
debug_log(f"{key_type.upper()}密钥备份到程序目录成功")
except Exception as e:
if DEBUG:
debug_log(f"备份失败: {e}")
if DEBUG:
debug_log("全局密钥检查/生成完成")
def get_global_rsa_private():
"""获取全局RSA私钥"""
key_path, _ = get_key_path(KEY_FILES['rsa_private'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, _ = get_key_path(KEY_FILES['rsa_private'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_rsa_public():
"""获取全局RSA公钥"""
key_path, _ = get_key_path(KEY_FILES['rsa_public'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, _ = get_key_path(KEY_FILES['rsa_public'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_x25519_private():
"""获取全局X25519私钥"""
key_path, _ = get_key_path(KEY_FILES['x25519_private'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, _ = get_key_path(KEY_FILES['x25519_private'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_x25519_public():
"""获取全局X25519公钥"""
key_path, _ = get_key_path(KEY_FILES['x25519_public'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, _ = get_key_path(KEY_FILES['x25519_public'])
with open(key_path, 'rb') as f:
return f.read()
# ==================== 加密笔记处理 ====================
class EncryptedNoteHandler:
"""加密笔记处理器 - 核心加密解密逻辑"""
@staticmethod
def check_file_has_password(file_path):
"""检查加密文件是否有密码保护"""
try:
with open(file_path, 'rb') as f:
data = f.read()
clean_data = remove_security_info(data)
json_str = clean_data.decode('utf-8')
info = json.loads(json_str)
return info.get('has_password', False), info.get('mode', 'aes_rsa'), info.get('single_key', False)
except Exception as e:
debug_error(f"检查文件密码状态失败: {e}")
return False, 'unknown', False
@staticmethod
def get_file_metadata(file_path):
"""获取加密文件的元数据(不进行解密)"""
try:
with open(file_path, 'rb') as f:
data = f.read()
clean_data = remove_security_info(data)
json_str = clean_data.decode('utf-8')
info = json.loads(json_str)
return {
'has_password': info.get('has_password', False),
'mode': info.get('mode', 'aes_rsa'),
'single_key': info.get('single_key', False),
'algorithm': info.get('algorithm', 'unknown'),
'timestamp': info.get('timestamp', 'unknown')
}
except Exception as e:
debug_error(f"获取文件元数据失败: {e}")
return None
@staticmethod
def encrypt_content(content: str, algorithm: str, key_mode: str, password: str = None,
single_private=None, single_public=None, strict_password=True):
"""
加密内容
content: 要加密的文本内容
algorithm: 'rsa' 或 'x25519'
key_mode: 'global' 或 'single'
password: 用户密码(可选)
single_private/single_public: 单独密钥对
strict_password: 是否使用高强度密码要求
"""
if DEBUG:
debug_log(f"开始加密 - 算法: {algorithm}, 密钥模式: {key_mode}, 密码保护: {password is not None}, 严格模式: {strict_password}")
debug_log(f"内容长度: {len(content)} 字符")
data = content.encode('utf-8')
private_key = None
public_key = None
try:
if algorithm == 'rsa':
if key_mode == 'global':
pub_pem = get_global_rsa_public()
if password:
encrypted = encrypt_text_aes_rsa_with_password(data, pub_pem, password, add_header=True, strict_password=strict_password)
else:
encrypted = encrypt_text_aes_rsa(data, pub_pem, add_header=True, strict_password=strict_password)
private_key = None
public_key = None
else:
if single_private is None or single_public is None:
private_key, public_key = generate_rsa_key(4096)
else:
private_key = single_private
public_key = single_public
if password:
encrypted = encrypt_text_aes_rsa_with_password(data, public_key, password, add_header=True, strict_password=strict_password)
else:
encrypted = encrypt_text_aes_rsa(data, public_key, add_header=True, strict_password=strict_password)
# 添加single_key标记
json_str = remove_security_info(encrypted).decode('utf-8')
info = json.loads(json_str)
info['single_key'] = True
encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_rsa', bool(password))
return encrypted, private_key, public_key
else: # x25519
if key_mode == 'global':
pub_raw = get_global_x25519_public()
if password:
encrypted = encrypt_text_aes_x25519(data, pub_raw, password, add_header=True, strict_password=strict_password)
else:
encrypted = encrypt_text_aes_x25519(data, pub_raw, add_header=True, strict_password=strict_password)
private_key = None
public_key = None
else:
if single_private is None or single_public is None:
private_key, public_key = generate_x25519_keys()
else:
private_key = single_private
public_key = single_public
if password:
encrypted = encrypt_text_aes_x25519(data, public_key, password, add_header=True, strict_password=strict_password)
else:
encrypted = encrypt_text_aes_x25519(data, public_key, add_header=True, strict_password=strict_password)
# 添加single_key标记
json_str = remove_security_info(encrypted).decode('utf-8')
info = json.loads(json_str)
info['single_key'] = True
encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_x25519', bool(password))
return encrypted, private_key, public_key
return encrypted, private_key, public_key
except Exception as e:
debug_error(f"加密过程异常: {e}")
raise Exception(f"加密失败: {str(e)}")
@staticmethod
def decrypt_file(file_path, password: str = None, single_private: bytes = None):
"""
解密文件
file_path: 文件路径
password: 用户密码(可选)
single_private: 单独私钥(可选)
"""
if DEBUG:
debug_log(f"开始解密文件: {file_path}")
debug_log(f"密码提供: {password is not None}, 单独私钥提供: {single_private is not None}")
if password:
debug_log(f"密码长度: {len(password)}")
try:
with open(file_path, 'rb') as f:
data = f.read()
clean_data = remove_security_info(data)
json_str = clean_data.decode('utf-8')
info = json.loads(json_str)
mode = info.get('mode', 'aes_rsa')
has_password = info.get('has_password', False)
single_key = info.get('single_key', False)
if DEBUG:
debug_log(f"加密信息 - 模式: {mode}, 有密码: {has_password}, 单文件密钥: {single_key}")
if mode in ['aes_rsa', 'aes_rsa_password_protected']:
if single_key:
if single_private is None:
raise ValueError("单独密钥_需要选择密钥文件")
priv = single_private
else:
priv = get_global_rsa_private()
if has_password:
if password is None:
raise ValueError("密码保护_需要输入密码")
decrypted = decrypt_text_aes_rsa_with_password(clean_data, priv, password, has_header=False)
else:
decrypted = decrypt_text_aes_rsa(clean_data, priv, has_header=False)
elif mode in ['aes_x25519', 'aes_x25519_password_protected']:
if single_key:
if single_private is None:
raise ValueError("单独密钥_需要选择密钥文件")
priv = single_private
else:
priv = get_global_x25519_private()
if has_password:
if password is None:
raise ValueError("密码保护_需要输入密码")
decrypted = decrypt_text_aes_x25519(clean_data, priv, password, has_header=False)
else:
decrypted = decrypt_text_aes_x25519(clean_data, priv, has_header=False)
else:
raise ValueError(f"未知加密模式: {mode}")
# 解密结果可能是 bytes 或 str,统一转为 bytes
if isinstance(decrypted, str):
decrypted = decrypted.encode('utf-8')
if DEBUG:
debug_log(f"解密成功,解密后数据大小: {len(decrypted)}字节")
return decrypted
except Exception as e:
debug_error(f"解密过程异常: {e}")
raise
# ==================== 通用解密函数(修复版:支持单独密钥文件选择) ====================
def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filename=None):
"""
解密.bjb文件的通用函数
file_path: 文件路径
parent_window: 父窗口(用于弹窗)
is_cloud_file: 是否为云端下载的文件
cloud_filename: 云端原始文件名(用于提示)
返回: (content, success)
"""
if DEBUG:
debug_log(f"通用解密函数: {file_path}, 云端文件: {is_cloud_file}")
display_name = cloud_filename if cloud_filename else os.path.basename(file_path)
try:
# 首先获取文件元数据,判断是否需要单独密钥
metadata = EncryptedNoteHandler.get_file_metadata(file_path)
if metadata is None:
QMessageBox.critical(parent_window, "文件错误", "无法读取文件元数据,文件可能已损坏")
return None, False
has_password = metadata['has_password']
single_key = metadata['single_key']
mode = metadata['mode']
if DEBUG:
debug_log(f"文件元数据 - has_password: {has_password}, single_key: {single_key}, mode: {mode}")
single_private = None
# ========== 处理单独密钥模式 ==========
if single_key:
if DEBUG:
debug_log("检测到单独密钥模式,需要查找或选择.key文件")
# 首先尝试在相同目录下查找对应的.key文件
key_file_path = file_path + '.key'
if os.path.exists(key_file_path):
if DEBUG:
debug_log(f"找到本地密钥文件: {key_file_path}")
try:
with open(key_file_path, 'rb') as f:
single_private = f.read()
if DEBUG:
debug_log(f"密钥文件读取成功,长度: {len(single_private)}字节")
except Exception as e:
debug_error(f"读取密钥文件失败: {e}")
QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}")
return None, False
else:
# 没有找到,弹出文件选择对话框
if DEBUG:
debug_log("未找到本地密钥文件,弹出选择对话框")
# 构建提示信息
msg = f"文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n"
msg += "请选择对应的私钥文件(.key):\n"
msg += "提示:此文件加密时会在同目录生成同名.key文件。"
if is_cloud_file:
msg = f"云端文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n"
msg += "服务器不存储您的私钥,请选择您本地保存的对应.key文件:\n"
msg += "提示:创建此文件时会在本地生成同名的.key文件。"
key_file_path, ok = QFileDialog.getOpenFileName(
parent_window,
"选择密钥文件 - " + display_name,
"",
"密钥文件 (*.key);;所有文件 (*.*)"
)
if not ok or not key_file_path:
QMessageBox.warning(parent_window, "密钥缺失",
f"无法解密「{display_name}」:缺少对应的私钥文件\n\n"
"此文件是使用【安全加密】模式创建的,\n"
"需要当初保存时生成的.key文件才能解密。\n"
"请找到对应的.key文件后重试。")
return None, False
try:
with open(key_file_path, 'rb') as f:
single_private = f.read()
if DEBUG:
debug_log(f"用户选择密钥文件: {key_file_path}, 长度: {len(single_private)}字节")
except Exception as e:
debug_error(f"读取用户选择的密钥文件失败: {e}")
QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}")
return None, False
# ========== 处理密码保护模式 ==========
if has_password:
if DEBUG:
debug_log("检测到密码保护,弹出密码输入框")
max_attempts = 3
attempts = 0
success = False
while attempts < max_attempts and not success:
pwd, ok = QInputDialog.getText(
parent_window, "输入密码 - " + display_name,
f"文件「{display_name}」受密码保护\n请输入密码 (尝试 {attempts + 1}/{max_attempts}):",
QLineEdit.Password
)
if not ok:
if DEBUG:
debug_log("用户取消输入密码")
return None, False
if pwd:
try:
if DEBUG:
debug_log(f"尝试密码解密 (长度: {len(pwd)})")
decrypted = EncryptedNoteHandler.decrypt_file(
file_path, password=pwd, single_private=single_private
)
if isinstance(decrypted, bytes):
content = decrypted.decode('utf-8')
else:
content = str(decrypted)
if DEBUG:
debug_log("密码解密成功")
return content, True
except ValueError as e:
error_msg = str(e)
if DEBUG:
debug_log(f"密码解密失败: {error_msg}")
if "单独密钥" in error_msg:
QMessageBox.warning(parent_window, "密钥错误",
"解密失败:密钥文件不匹配\n请选择正确的.key文件")
return None, False
attempts += 1
if attempts < max_attempts:
QMessageBox.warning(parent_window, "密码错误",
f"密码错误,还剩 {max_attempts - attempts} 次尝试")
else:
QMessageBox.warning(parent_window, "解密失败",
"多次密码错误,无法解密文件")
return None, False
except Exception as e:
error_msg = str(e)
if DEBUG:
debug_log(f"解密异常: {error_msg}")
attempts += 1
if attempts < max_attempts:
QMessageBox.warning(parent_window, "解密失败",
f"解密失败: {error_msg}\n还剩 {max_attempts - attempts} 次尝试")
else:
QMessageBox.warning(parent_window, "解密失败",
f"多次尝试失败,无法解密文件\n错误: {error_msg}")
return None, False
else:
attempts += 1
QMessageBox.warning(parent_window, "密码为空", "密码不能为空")
return None, False
else:
# ========== 无密码保护,直接解密 ==========
if DEBUG:
debug_log("无密码保护,直接解密")
try:
decrypted = EncryptedNoteHandler.decrypt_file(
file_path, password=None, single_private=single_private
)
if isinstance(decrypted, bytes):
content = decrypted.decode('utf-8')
else:
content = str(decrypted)
return content, True
except ValueError as e:
error_msg = str(e)
if "单独密钥" in error_msg:
QMessageBox.warning(parent_window, "密钥错误",
"解密失败:需要对应的.key密钥文件\n请选择正确的.key文件")
else:
QMessageBox.warning(parent_window, "解密失败", str(e))
return None, False
except Exception as e:
debug_error(f"解密失败: {e}")
QMessageBox.critical(parent_window, "解密失败", str(e))
return None, False
except Exception as e:
debug_error(f"解密过程异常: {e}")
QMessageBox.critical(parent_window, "解密失败", str(e))
return None, False
# ==================== 云端API客户端 ====================
class CloudClient:
"""云端API客户端,处理所有网络请求"""
def __init__(self):
if DEBUG:
debug_log("初始化CloudClient")
self.token = None
self.user_id = None
self.username = None
def set_auth(self, token, user_id, username):
"""设置认证信息"""
if DEBUG:
debug_log(f"设置认证信息 - 用户: {username}, ID: {user_id}")
self.token = token
self.user_id = user_id
self.username = username
def clear_auth(self):
"""清除认证信息"""
if DEBUG:
debug_log("清除认证信息")
self.token = None
self.user_id = None
self.username = None
def _headers(self):
"""获取请求头"""
if self.token:
return {'x-access-token': self.token}
return {}
def _request(self, method, endpoint, **kwargs):
"""发送HTTP请求"""
url = f"{API_BASE_URL}{endpoint}"
headers = self._headers()
if 'headers' in kwargs:
headers.update(kwargs.pop('headers'))
try:
resp = requests.request(method, url, headers=headers, timeout=30, **kwargs)
if resp.status_code == 401:
return None
return resp
except requests.exceptions.ConnectionError:
QMessageBox.critical(None, "网络错误", "无法连接到服务器,请检查网络连接")
return None
except Exception as e:
QMessageBox.critical(None, "网络错误", f"请求失败: {str(e)}")
return None
def register(self, username, email, password):
"""用户注册"""
resp = self._request('POST', '/register', json={'username': username, 'email': email, 'password': password})
if resp and resp.status_code == 201:
return resp.json()
return None
def login(self, email, password):
"""用户登录"""
resp = self._request('POST', '/login', json={'email': email, 'password': password})
if resp and resp.status_code == 200:
data = resp.json()
self.set_auth(data['token'], data['user_id'], data['username'])
return data
return None
def get_filetree(self, folder='/'):
"""获取文件树"""
resp = self._request('GET', '/filetree', params={'folder': folder})
if resp and resp.status_code == 200:
return resp.json()
return None
def create_folder(self, name, parent='/'):
"""创建文件夹"""
resp = self._request('POST', '/create_folder', json={'name': name, 'parent': parent})
if resp and resp.status_code == 201:
return resp.json()
return None
def upload_file(self, file_path, parent_folder='/'):
"""上传文件"""
ext = os.path.splitext(file_path)[1].lower()
if ext not in ['.txt', '.bjb']:
QMessageBox.warning(None, "错误", "只支持上传 .txt 和 .bjb 文件")
return None
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f)}
data = {'parent_folder': parent_folder}
resp = self._request('POST', '/upload', files=files, data=data)
if resp and resp.status_code == 201:
return resp.json()
return None
def download_file(self, file_id, target_path):
"""下载文件"""
resp = self._request('GET', f'/download/{file_id}')
if resp and resp.status_code == 200:
with open(target_path, 'wb') as f:
f.write(resp.content)
return True
return False
def delete_item(self, item_id):
"""删除文件或文件夹"""
resp = self._request('DELETE', f'/delete/{item_id}')
if resp:
return resp.status_code == 200
return False
def rename_item(self, item_id, new_name):
"""重命名文件或文件夹"""
resp = self._request('PUT', f'/rename/{item_id}', json={'new_name': new_name})
if resp:
return resp.status_code == 200
return False
def move_item(self, item_id, target_folder):
"""移动文件或文件夹"""
resp = self._request('POST', '/move', json={'item_id': item_id, 'target_folder': target_folder})
if resp:
return resp.status_code == 200
return False
def search_files(self, keyword):
"""搜索文件"""
resp = self._request('GET', '/search', params={'q': keyword})
if resp and resp.status_code == 200:
return resp.json()
return None
def get_storage_info(self):
"""获取存储信息"""
resp = self._request('GET', '/storage_info')
if resp and resp.status_code == 200:
return resp.json()
return None
def get_user_info(self):
"""获取用户信息"""
resp = self._request('GET', '/user_info')
if resp and resp.status_code == 200:
return resp.json()
return None
# ==================== 登录对话框 ====================
class LoginDialog(QDialog):
"""登录对话框"""
def __init__(self, client, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化登录对话框")
self.client = client
self.setWindowTitle("云笔记 - 登录")
self.setFixedSize(400, 350)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setup_ui()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
card = QWidget()
card.setObjectName("card")
card.setStyleSheet("""
#card { background-color: white; border-radius: 16px; }
QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; }
QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; }
""")
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(40, 40, 40, 40)
card_layout.setSpacing(20)
title = QLabel("☁️ 云笔记")
title.setStyleSheet("font-size: 28px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
card_layout.addWidget(title)
self.email_input = QLineEdit()
self.email_input.setPlaceholderText("电子邮箱")
self.email_input.setMinimumHeight(45)
card_layout.addWidget(self.email_input)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("密码")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setMinimumHeight(45)
card_layout.addWidget(self.password_input)
self.remember_check = QCheckBox("记住我")
card_layout.addWidget(self.remember_check)
self.login_btn = QPushButton("登 录")
self.login_btn.setMinimumHeight(45)
self.login_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;")
self.login_btn.clicked.connect(self.do_login)
card_layout.addWidget(self.login_btn)
register_btn = QPushButton("还没有账号?立即注册")
register_btn.setStyleSheet("background: transparent; color: #0366d6; border: none;")
register_btn.clicked.connect(self.show_register)
card_layout.addWidget(register_btn, alignment=Qt.AlignCenter)
close_btn = QPushButton("×")
close_btn.setFixedSize(30, 30)
close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;")
close_btn.clicked.connect(self.reject)
layout.addWidget(card)
close_btn.setParent(self)
close_btn.move(self.width() - 40, 15)
def do_login(self):
"""执行登录"""
email = self.email_input.text().strip()
password = self.password_input.text().strip()
if not email or not password:
QMessageBox.warning(self, "提示", "请填写邮箱和密码")
return
data = self.client.login(email, password)
if data:
if self.remember_check.isChecked():
settings = QSettings("CloudNote", "User")
settings.setValue("token", self.client.token)
settings.setValue("user_id", self.client.user_id)
settings.setValue("username", self.client.username)
self.accept()
else:
QMessageBox.warning(self, "错误", "邮箱或密码错误")
def show_register(self):
"""显示注册对话框"""
dialog = RegisterDialog(self.client, self)
dialog.exec_()
# ==================== 注册对话框 ====================
class RegisterDialog(QDialog):
"""注册对话框"""
def __init__(self, client, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化注册对话框")
self.client = client
self.setWindowTitle("注册账号")
self.setFixedSize(400, 450)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setup_ui()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
card = QWidget()
card.setObjectName("card")
card.setStyleSheet("""
#card { background-color: white; border-radius: 16px; }
QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; }
QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; }
""")
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(40, 40, 40, 40)
card_layout.setSpacing(15)
title = QLabel("📝 创建新账号")
title.setStyleSheet("font-size: 24px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
card_layout.addWidget(title)
self.username_input = QLineEdit()
self.username_input.setPlaceholderText("用户名")
self.username_input.setMinimumHeight(45)
card_layout.addWidget(self.username_input)
self.email_input = QLineEdit()
self.email_input.setPlaceholderText("电子邮箱")
self.email_input.setMinimumHeight(45)
card_layout.addWidget(self.email_input)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("密码(至少6位)")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setMinimumHeight(45)
card_layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setMinimumHeight(45)
card_layout.addWidget(self.confirm_input)
self.register_btn = QPushButton("注 册")
self.register_btn.setMinimumHeight(45)
self.register_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;")
self.register_btn.clicked.connect(self.do_register)
card_layout.addWidget(self.register_btn)
close_btn = QPushButton("×")
close_btn.setFixedSize(30, 30)
close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;")
close_btn.clicked.connect(self.reject)
layout.addWidget(card)
close_btn.setParent(self)
close_btn.move(self.width() - 40, 15)
def do_register(self):
"""执行注册"""
username = self.username_input.text().strip()
email = self.email_input.text().strip()
password = self.password_input.text().strip()
confirm = self.confirm_input.text().strip()
if not all([username, email, password]):
QMessageBox.warning(self, "提示", "请填写所有字段")
return
if password != confirm:
QMessageBox.warning(self, "错误", "两次密码不一致")
return
if len(password) < 6:
QMessageBox.warning(self, "错误", "密码至少6位")
return
res = self.client.register(username, email, password)
if res:
QMessageBox.information(self, "成功", "注册成功,请登录")
self.accept()
else:
QMessageBox.warning(self, "错误", "注册失败,用户名或邮箱已存在")
# ==================== 新建加密文件对话框 ====================
class NewEncryptedFileDialog(QDialog):
"""新建加密文件配置对话框"""
def __init__(self, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化新建加密文件对话框")
self.setWindowTitle("新建加密笔记")
self.setFixedSize(600, 700)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.strict_password = True
self.custom_password = None
self.single_private = None
self.single_public = None
self.setup_ui()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
layout.setSpacing(15)
title = QLabel("🔐 加密配置")
title.setStyleSheet("font-size: 18px; font-weight: bold;")
layout.addWidget(title)
group1 = QGroupBox("1. 选择加密算法")
group1_layout = QVBoxLayout()
self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM")
self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM")
self.radio_rsa.setChecked(True)
group1_layout.addWidget(self.radio_rsa)
group1_layout.addWidget(self.radio_x25519)
group1.setLayout(group1_layout)
layout.addWidget(group1)
group2 = QGroupBox("2. 选择密钥方式")
group2_layout = QVBoxLayout()
self.radio_global = QRadioButton("默认加密(使用全局密钥对)")
self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)")
self.radio_global.setChecked(True)
group2_layout.addWidget(self.radio_global)
group2_layout.addWidget(self.radio_single)
group2.setLayout(group2_layout)
layout.addWidget(group2)
group3 = QGroupBox("3. 额外密码保护")
group3_layout = QVBoxLayout()
strength_layout = QHBoxLayout()
strength_layout.addWidget(QLabel("密码强度要求:"))
self.radio_strict = QRadioButton("高强度 (推荐)")
self.radio_weak = QRadioButton("低强度 (不推荐)")
self.radio_strict.setChecked(True)
strength_layout.addWidget(self.radio_strict)
strength_layout.addWidget(self.radio_weak)
group3_layout.addLayout(strength_layout)
self.checkbox_password = QCheckBox("启用双层加密")
self.checkbox_password.toggled.connect(self.on_password_toggled)
group3_layout.addWidget(self.checkbox_password)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setEnabled(False)
group3_layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setEnabled(False)
group3_layout.addWidget(self.confirm_input)
self.password_strength_label = QLabel("")
self.password_strength_label.setStyleSheet("font-size: 12px;")
group3_layout.addWidget(self.password_strength_label)
self.password_input.textChanged.connect(self.check_password_strength)
self.radio_strict.toggled.connect(self.on_strength_mode_changed)
self.radio_weak.toggled.connect(self.on_strength_mode_changed)
group3.setLayout(group3_layout)
layout.addWidget(group3)
info_label = QLabel("说明:\n• 默认加密:使用全局密钥对,所有文件共用同一密钥\n• 安全加密:每个文件独立生成密钥对,私钥保存为 .key 文件\n• 密钥文件请妥善保管,丢失后将无法解密文件!\n• 高强度密码要求≥16位含大小写+数字+特殊字符\n• 低强度模式仅用于测试,不推荐用于重要数据")
info_label.setStyleSheet("color: #586069; font-size: 12px; background-color: #f6f8fa; padding: 10px; border-radius: 8px;")
info_label.setWordWrap(True)
layout.addWidget(info_label)
btn_layout = QHBoxLayout()
self.ok_btn = QPushButton("确定")
self.cancel_btn = QPushButton("取消")
self.ok_btn.clicked.connect(self.accept_config)
self.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
def on_strength_mode_changed(self):
"""密码强度模式改变"""
self.strict_password = self.radio_strict.isChecked()
if self.radio_strict.isChecked():
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
else:
self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)")
self.check_password_strength()
def on_password_toggled(self, checked):
"""密码开关切换"""
self.password_input.setEnabled(checked)
self.confirm_input.setEnabled(checked)
self.use_password = checked
if checked:
self.check_password_strength()
else:
self.password_strength_label.setText("")
def check_password_strength(self):
"""检查密码强度"""
if not self.use_password:
self.password_strength_label.setText("")
return
pwd = self.password_input.text()
if self.radio_strict.isChecked():
if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd):
self.password_strength_label.setText("✓ 密码强度:强")
self.password_strength_label.setStyleSheet("color: green; font-size: 12px;")
elif len(pwd) >= 12:
self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位,包含大小写+数字+特殊字符)")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
else:
self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位,包含大小写+数字+特殊字符)")
self.password_strength_label.setStyleSheet("color: red; font-size: 12px;")
else:
if len(pwd) >= 8:
self.password_strength_label.setText("✓ 密码已输入(低强度模式,建议使用高强度)")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
elif len(pwd) > 0:
self.password_strength_label.setText("⚠ 密码太短(建议至少8位)")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
else:
self.password_strength_label.setText("请输入密码")
self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;")
def accept_config(self):
"""接受配置"""
if DEBUG:
debug_log("开始获取加密配置")
if self.radio_rsa.isChecked():
self.selected_algorithm = 'rsa'
else:
self.selected_algorithm = 'x25519'
if DEBUG:
debug_log(f"算法: {self.selected_algorithm}")
if self.radio_global.isChecked():
self.selected_key_mode = 'global'
else:
self.selected_key_mode = 'single'
if DEBUG:
debug_log(f"密钥模式: {self.selected_key_mode}")
self.strict_password = self.radio_strict.isChecked()
if DEBUG:
debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}")
if self.use_password:
pwd = self.password_input.text()
confirm = self.confirm_input.text()
if not pwd or not confirm:
QMessageBox.warning(self, "提示", "请输入密码")
return
if pwd != confirm:
QMessageBox.warning(self, "错误", "两次输入的密码不一致")
return
if self.strict_password and not is_password_strong(pwd, strict=True):
QMessageBox.warning(self, "密码强度不足",
"密码必须至少16位,包含大小写字母、数字和特殊字符\n\n"
"或切换到「低强度」模式(不推荐)")
return
self.custom_password = pwd
if DEBUG:
debug_log("密码验证通过")
if self.selected_key_mode == 'single':
if DEBUG:
debug_log("生成单独密钥对")
if self.selected_algorithm == 'rsa':
self.single_private, self.single_public = generate_rsa_key(4096)
else:
self.single_private, self.single_public = generate_x25519_keys()
if DEBUG:
debug_log("加密配置获取完成")
self.accept()
def get_config(self):
"""获取配置"""
return {
'algorithm': self.selected_algorithm,
'key_mode': self.selected_key_mode,
'use_password': self.use_password,
'strict_password': self.strict_password,
'custom_password': self.custom_password,
'single_private': self.single_private,
'single_public': self.single_public
}
# ==================== 保存加密文件对话框 ====================
class SaveEncryptedFileDialog(QDialog):
"""保存加密文件对话框"""
def __init__(self, content, algorithm, key_mode, use_password, strict_password,
custom_password, single_private, single_public, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化保存加密文件对话框")
self.content = content
self.algorithm = algorithm
self.key_mode = key_mode
self.use_password = use_password
self.strict_password = strict_password
self.custom_password = custom_password
self.single_private = single_private
self.single_public = single_public
self.setWindowTitle("保存加密笔记")
self.setFixedSize(650, 400)
self.selected_file_path = None
self.setup_ui()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
layout.setSpacing(15)
if self.key_mode == 'single':
info_label = QLabel("⚠️ 您选择了【安全加密】模式,将为当前文件单独生成密钥对。\n密钥文件(.key)将与.bjb文件保存在同一目录,请妥善保管!\n丢失密钥文件后将无法解密!")
info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 10px; border-radius: 8px;")
info_label.setWordWrap(True)
layout.addWidget(info_label)
layout.addWidget(QLabel("📁 保存位置和文件名:"))
file_select_layout = QHBoxLayout()
self.file_path_input = QLineEdit()
self.file_path_input.setPlaceholderText("请选择保存位置...")
self.file_path_input.setMinimumHeight(35)
self.file_path_input.textChanged.connect(self.on_path_changed)
file_select_layout.addWidget(self.file_path_input)
self.browse_btn = QPushButton("浏览...")
self.browse_btn.clicked.connect(self.select_save_file)
file_select_layout.addWidget(self.browse_btn)
layout.addLayout(file_select_layout)
info_group = QGroupBox("加密信息摘要")
info_layout = QVBoxLayout()
algo_text = "RSA-4096" if self.algorithm == 'rsa' else "X25519"
key_text = "全局密钥" if self.key_mode == 'global' else "单独密钥(安全模式)"
pwd_text = "已启用" if self.use_password else "未启用"
strength_text = "高强度" if self.strict_password else "低强度"
info_layout.addWidget(QLabel(f"🔐 加密算法: {algo_text} + AES-256-GCM"))
info_layout.addWidget(QLabel(f"🔑 密钥方式: {key_text}"))
info_layout.addWidget(QLabel(f"🔒 密码保护: {pwd_text}"))
if self.use_password:
info_layout.addWidget(QLabel(f"🔐 密码强度: {strength_text}"))
info_group.setLayout(info_layout)
layout.addWidget(info_group)
btn_layout = QHBoxLayout()
self.ok_btn = QPushButton("💾 保存")
self.ok_btn.setStyleSheet("background-color: #28a745; color: white;")
self.cancel_btn = QPushButton("取消")
self.ok_btn.clicked.connect(self.do_save)
self.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
def on_path_changed(self, text):
"""路径改变"""
if text.strip():
self.selected_file_path = text.strip()
def select_save_file(self):
"""选择保存文件"""
file_path, _ = QFileDialog.getSaveFileName(self, "保存加密笔记", "", "加密笔记 (*.bjb)")
if file_path:
if not file_path.endswith('.bjb'):
file_path += '.bjb'
self.selected_file_path = file_path
self.file_path_input.setText(file_path)
def do_save(self):
"""执行保存"""
if not self.selected_file_path:
QMessageBox.warning(self, "提示", "请选择保存位置")
return
if not self.selected_file_path.endswith('.bjb'):
self.selected_file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
self.content, self.algorithm, self.key_mode,
self.custom_password, self.single_private, self.single_public,
strict_password=self.strict_password
)
except Exception as e:
QMessageBox.critical(self, "加密失败", str(e))
return
try:
with open(self.selected_file_path, 'wb') as f:
f.write(encrypted_data)
except Exception as e:
QMessageBox.critical(self, "保存失败", f"无法保存.bjb文件: {str(e)}")
return
key_path = None
if self.key_mode == 'single' and priv:
key_path = self.selected_file_path + '.key'
try:
with open(key_path, 'wb') as f:
f.write(priv)
except Exception as e:
QMessageBox.critical(self, "保存失败", f"无法保存.key文件: {str(e)}")
return
msg = f"✅ 文件已保存!\n\n📄 .bjb文件:{self.selected_file_path}"
if key_path:
msg += f"\n🔑 .key文件:{key_path}\n\n⚠️ 请妥善保管密钥文件!"
else:
msg += "\n\n💡 提示:使用全局密钥,无需额外保存密钥文件。"
QMessageBox.information(self, "保存成功", msg)
self.accept()
# ==================== 云端文件树窗口 ====================
class CloudFileTreeDialog(QDialog):
"""云端文件管理对话框"""
def __init__(self, client, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化云端文件树对话框")
self.client = client
self.setWindowTitle("云端文件管理")
self.resize(900, 700)
self.setup_ui()
self.current_folder = '/'
self.load_filetree()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
toolbar = QToolBar()
self.up_btn = QAction("⬆ 返回上级", self)
self.up_btn.triggered.connect(self.go_up)
toolbar.addAction(self.up_btn)
self.refresh_btn = QAction("🔄 刷新", self)
self.refresh_btn.triggered.connect(self.load_filetree)
toolbar.addAction(self.refresh_btn)
self.new_folder_btn = QAction("📁 新建文件夹", self)
self.new_folder_btn.triggered.connect(self.create_folder)
toolbar.addAction(self.new_folder_btn)
self.upload_btn = QAction("📤 上传文件", self)
self.upload_btn.triggered.connect(self.upload_file)
toolbar.addAction(self.upload_btn)
layout.addWidget(toolbar)
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("搜索文件...")
self.search_input.returnPressed.connect(self.search_files)
search_btn = QPushButton("搜索")
search_btn.clicked.connect(self.search_files)
clear_btn = QPushButton("清除")
clear_btn.clicked.connect(self.clear_search)
search_layout.addWidget(self.search_input)
search_layout.addWidget(search_btn)
search_layout.addWidget(clear_btn)
layout.addLayout(search_layout)
self.tree = QTreeWidget()
self.tree.setHeaderLabels(["名称", "大小", "类型", "修改时间"])
self.tree.setAlternatingRowColors(True)
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.show_context_menu)
self.tree.itemDoubleClicked.connect(self.on_item_double_click)
layout.addWidget(self.tree)
self.status_label = QLabel("")
layout.addWidget(self.status_label)
self.load_storage_info()
def load_storage_info(self):
"""加载存储信息"""
info = self.client.get_storage_info()
if info:
used_mb = info['total_size'] / (1024 * 1024)
max_mb = info['max_size'] / (1024 * 1024)
download_used_mb = info['download_used'] / (1024 * 1024)
download_max_mb = info['download_max'] / (1024 * 1024)
self.status_label.setText(f"存储: {used_mb:.1f}MB / {max_mb:.0f}MB | 今日下载: {download_used_mb:.1f}MB / {download_max_mb:.0f}MB")
def load_filetree(self):
"""加载文件树"""
if DEBUG:
debug_log(f"加载文件树: {self.current_folder}")
data = self.client.get_filetree(self.current_folder)
if not data:
QMessageBox.warning(self, "错误", "加载文件树失败")
return
self.tree.clear()
for item in data['items']:
size_str = self.format_size(item['size']) if item['size'] else ""
type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件")
tree_item = QTreeWidgetItem([item['name'], size_str, type_str, item['created_at']])
tree_item.setData(0, Qt.UserRole, item)
self.tree.addTopLevelItem(tree_item)
self.load_storage_info()
def format_size(self, size_bytes):
"""格式化文件大小"""
if not size_bytes:
return ""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f}{unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f}TB"
def go_up(self):
"""返回上级目录"""
if self.current_folder != '/':
parts = self.current_folder.rstrip('/').split('/')
if len(parts) > 1:
self.current_folder = '/' + '/'.join(parts[:-1])
else:
self.current_folder = '/'
self.load_filetree()
def create_folder(self):
"""创建文件夹"""
name, ok = QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:")
if ok and name:
if '/' in name or '\\' in name:
QMessageBox.warning(self, "错误", "文件夹名称不能包含路径分隔符")
return
res = self.client.create_folder(name, self.current_folder)
if res:
self.load_filetree()
else:
QMessageBox.warning(self, "错误", "创建失败")
def upload_file(self):
"""上传文件"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择要上传的文件", "", "笔记本文件 (*.bjb *.txt)")
if file_path:
ext = os.path.splitext(file_path)[1].lower()
if ext not in ['.txt', '.bjb']:
QMessageBox.warning(self, "错误", "只支持上传 .txt 和 .bjb 文件")
return
file_size = os.path.getsize(file_path)
info = self.client.get_storage_info()
if info and info['total_size'] + file_size > info['max_size']:
QMessageBox.warning(self, "错误", "云端存储空间不足")
return
res = self.client.upload_file(file_path, self.current_folder)
if res:
QMessageBox.information(self, "成功", "上传成功")
self.load_filetree()
else:
QMessageBox.warning(self, "错误", "上传失败")
def search_files(self):
"""搜索文件"""
keyword = self.search_input.text().strip()
if not keyword:
return
data = self.client.search_files(keyword)
if data:
self.tree.clear()
for item in data['items']:
size_str = self.format_size(item['size']) if item['size'] else ""
type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件")
tree_item = QTreeWidgetItem([item['name'], size_str, type_str, ""])
tree_item.setData(0, Qt.UserRole, item)
self.tree.addTopLevelItem(tree_item)
self.status_label.setText(f"搜索到 {len(data['items'])} 个结果")
def clear_search(self):
"""清除搜索"""
self.search_input.clear()
self.load_filetree()
def on_item_double_click(self, item, col):
"""双击项目"""
data = item.data(0, Qt.UserRole)
if data['is_folder']:
if self.search_input.text():
self.clear_search()
self.current_folder = data['parent'] + '/' + data['name'] if data['parent'] != '/' else '/' + data['name']
self.load_filetree()
else:
self.download_and_open(data)
def download_and_open(self, file_info):
"""
下载并打开云端文件
修复:支持单独密钥模式,弹出文件选择对话框让用户选择.key文件
"""
temp_dir = tempfile.gettempdir()
local_path = os.path.join(temp_dir, f"cloud_{file_info['id']}_{file_info['name']}")
if DEBUG:
debug_log(f"下载文件到: {local_path}")
debug_log(f"文件信息: ID={file_info['id']}, Name={file_info['name']}, Type={file_info.get('type', 'unknown')}")
if self.client.download_file(file_info['id'], local_path):
if DEBUG:
debug_log("下载成功")
ext = os.path.splitext(file_info['name'])[1].lower()
if ext == '.txt':
# 文本文件直接打开
try:
with open(local_path, 'r', encoding='utf-8') as f:
content = f.read()
main_window = self.parent()
if isinstance(main_window, MainWindow):
tab = EditorTab(main_window.tabs, main_window.tabs.count(),
file_path=local_path, cloud_id=file_info['id'],
file_type='txt', content=content)
main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name']))
main_window.editor_tabs.append(tab)
main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1)
self.accept()
except Exception as e:
debug_error(f"读取文本文件失败: {e}")
QMessageBox.warning(self, "错误", f"读取文件失败: {str(e)}")
else:
# 加密文件:使用修复后的解密函数,传入云端文件标志和文件名
if DEBUG:
debug_log("调用解密函数(云端文件模式)")
# 先获取文件元数据,看看是否需要单独密钥
metadata = EncryptedNoteHandler.get_file_metadata(local_path)
if metadata and metadata.get('single_key', False):
if DEBUG:
debug_log("检测到单独密钥模式,准备弹出密钥文件选择对话框")
content, success = decrypt_bjb_file(
local_path, self,
is_cloud_file=True,
cloud_filename=file_info['name']
)
if success and content is not None:
if DEBUG:
debug_log("解密成功,创建编辑器选项卡")
main_window = self.parent()
if isinstance(main_window, MainWindow):
tab = EditorTab(main_window.tabs, main_window.tabs.count(),
file_path=local_path, cloud_id=file_info['id'],
file_type='bjb', content=content)
main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name']))
main_window.editor_tabs.append(tab)
main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1)
self.accept()
else:
if DEBUG:
debug_log(f"警告: parent 不是 MainWindow 类型: {type(main_window)}")
else:
if DEBUG:
debug_log("解密失败,不关闭对话框")
# 解密失败时不关闭对话框,让用户重试
else:
if DEBUG:
debug_log("下载失败")
QMessageBox.warning(self, "错误", "下载失败")
def show_context_menu(self, pos):
"""显示右键菜单"""
item = self.tree.itemAt(pos)
if not item:
return
data = item.data(0, Qt.UserRole)
menu = QMenu()
rename_action = menu.addAction("重命名")
delete_action = menu.addAction("删除")
move_action = menu.addAction("移动")
download_action = menu.addAction("下载")
action = menu.exec_(self.tree.viewport().mapToGlobal(pos))
if action == rename_action:
new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=data['name'])
if ok and new_name:
if self.client.rename_item(data['id'], new_name):
self.load_filetree()
else:
QMessageBox.warning(self, "错误", "重命名失败")
elif action == delete_action:
if QMessageBox.question(self, "确认", f"确定删除 {data['name']} 吗?") == QMessageBox.Yes:
if self.client.delete_item(data['id']):
self.load_filetree()
else:
QMessageBox.warning(self, "错误", "删除失败")
elif action == move_action:
target, ok = QInputDialog.getText(self, "移动", "目标文件夹路径 (例如 /folder):")
if ok and target:
if self.client.move_item(data['id'], target):
self.load_filetree()
else:
QMessageBox.warning(self, "错误", "移动失败")
elif action == download_action:
save_path, _ = QFileDialog.getSaveFileName(self, "保存文件", data['name'])
if save_path:
if self.client.download_file(data['id'], save_path):
QMessageBox.information(self, "成功", "下载完成")
else:
QMessageBox.warning(self, "错误", "下载失败")
# ==================== 编辑器选项卡 ====================
class EditorTab:
"""编辑器选项卡类"""
def __init__(self, tab_widget, index, file_path=None, cloud_id=None, is_new=False,
content="", file_type="txt", encrypt_config=None):
if DEBUG:
debug_log(f"创建编辑器选项卡 - 类型: {file_type}, 新文件: {is_new}, 路径: {file_path}")
self.tab_widget = tab_widget
self.index = index
self.file_path = file_path
self.cloud_id = cloud_id
self.is_new = is_new
self.file_type = file_type
self.encrypt_config = encrypt_config
self.modified = False
self.text_edit = QTextEdit()
self.text_edit.setPlainText(content)
self.text_edit.textChanged.connect(self.on_text_changed)
font = QFont("Consolas", 12)
self.text_edit.setFont(font)
self.update_tab_title()
def update_tab_title(self):
"""更新选项卡标题"""
if self.file_path:
name = os.path.basename(self.file_path)
elif self.cloud_id is not None:
name = f"云端文件_{self.cloud_id}"
else:
name = "未命名"
if self.file_type == 'bjb':
name = f"🔒 {name}"
if self.modified:
name = "* " + name
self.tab_widget.setTabText(self.index, name)
def on_text_changed(self):
"""文本改变时的回调"""
if not self.modified:
self.modified = True
self.update_tab_title()
def get_content(self):
"""获取内容"""
return self.text_edit.toPlainText()
def set_content(self, content):
"""设置内容"""
self.text_edit.setPlainText(content)
self.modified = False
self.update_tab_title()
def mark_saved(self):
"""标记已保存"""
self.modified = False
self.update_tab_title()
# ==================== 加密当前文件对话框 ====================
class EncryptCurrentFileDialog(QDialog):
"""加密当前文件对话框"""
def __init__(self, parent=None):
super().__init__(parent)
if DEBUG:
debug_log("初始化加密当前文件对话框")
self.setWindowTitle("加密当前文件")
self.setFixedSize(600, 600)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.strict_password = True
self.custom_password = None
self.single_private = None
self.single_public = None
self.setup_ui()
def setup_ui(self):
"""设置UI"""
layout = QVBoxLayout(self)
layout.setSpacing(15)
title = QLabel("🔐 加密当前文件配置")
title.setStyleSheet("font-size: 18px; font-weight: bold;")
layout.addWidget(title)
info_label = QLabel("⚠️ 注意:加密后将生成新的.bjb文件,原文件不会被删除")
info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 8px; border-radius: 8px;")
layout.addWidget(info_label)
group1 = QGroupBox("1. 选择加密算法")
group1_layout = QVBoxLayout()
self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM")
self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM")
self.radio_rsa.setChecked(True)
group1_layout.addWidget(self.radio_rsa)
group1_layout.addWidget(self.radio_x25519)
group1.setLayout(group1_layout)
layout.addWidget(group1)
group2 = QGroupBox("2. 选择密钥方式")
group2_layout = QVBoxLayout()
self.radio_global = QRadioButton("默认加密(使用全局密钥对)")
self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)")
self.radio_global.setChecked(True)
group2_layout.addWidget(self.radio_global)
group2_layout.addWidget(self.radio_single)
group2.setLayout(group2_layout)
layout.addWidget(group2)
group3 = QGroupBox("3. 额外密码保护")
group3_layout = QVBoxLayout()
strength_layout = QHBoxLayout()
strength_layout.addWidget(QLabel("密码强度要求:"))
self.radio_strict = QRadioButton("高强度 (推荐)")
self.radio_weak = QRadioButton("低强度 (不推荐)")
self.radio_strict.setChecked(True)
strength_layout.addWidget(self.radio_strict)
strength_layout.addWidget(self.radio_weak)
group3_layout.addLayout(strength_layout)
self.checkbox_password = QCheckBox("启用双层加密")
self.checkbox_password.toggled.connect(self.on_password_toggled)
group3_layout.addWidget(self.checkbox_password)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setEnabled(False)
group3_layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setEnabled(False)
group3_layout.addWidget(self.confirm_input)
self.password_strength_label = QLabel("")
self.password_strength_label.setStyleSheet("font-size: 12px;")
group3_layout.addWidget(self.password_strength_label)
self.password_input.textChanged.connect(self.check_password_strength)
self.radio_strict.toggled.connect(self.on_strength_mode_changed)
group3.setLayout(group3_layout)
layout.addWidget(group3)
btn_layout = QHBoxLayout()
self.ok_btn = QPushButton("加密并保存")
self.ok_btn.setStyleSheet("background-color: #28a745; color: white;")
self.cancel_btn = QPushButton("取消")
self.ok_btn.clicked.connect(self.accept_config)
self.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
def on_strength_mode_changed(self):
"""密码强度模式改变"""
self.strict_password = self.radio_strict.isChecked()
if self.radio_strict.isChecked():
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
else:
self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)")
self.check_password_strength()
def on_password_toggled(self, checked):
"""密码开关切换"""
self.password_input.setEnabled(checked)
self.confirm_input.setEnabled(checked)
self.use_password = checked
if checked:
self.check_password_strength()
else:
self.password_strength_label.setText("")
def check_password_strength(self):
"""检查密码强度"""
if not self.use_password:
self.password_strength_label.setText("")
return
pwd = self.password_input.text()
if self.radio_strict.isChecked():
if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd):
self.password_strength_label.setText("✓ 密码强度:强")
self.password_strength_label.setStyleSheet("color: green; font-size: 12px;")
elif len(pwd) >= 12:
self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位)")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
else:
self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位)")
self.password_strength_label.setStyleSheet("color: red; font-size: 12px;")
else:
if len(pwd) >= 8:
self.password_strength_label.setText("✓ 密码已输入(低强度模式)")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
elif len(pwd) > 0:
self.password_strength_label.setText("⚠ 密码太短")
self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;")
else:
self.password_strength_label.setText("请输入密码")
self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;")
def accept_config(self):
"""接受配置"""
if DEBUG:
debug_log("开始获取加密配置")
if self.radio_rsa.isChecked():
self.selected_algorithm = 'rsa'
else:
self.selected_algorithm = 'x25519'
if DEBUG:
debug_log(f"算法: {self.selected_algorithm}")
if self.radio_global.isChecked():
self.selected_key_mode = 'global'
else:
self.selected_key_mode = 'single'
if DEBUG:
debug_log(f"密钥模式: {self.selected_key_mode}")
self.strict_password = self.radio_strict.isChecked()
if DEBUG:
debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}")
if self.use_password:
pwd = self.password_input.text()
confirm = self.confirm_input.text()
if not pwd or not confirm:
QMessageBox.warning(self, "提示", "请输入密码")
return
if pwd != confirm:
QMessageBox.warning(self, "错误", "两次输入的密码不一致")
return
if self.strict_password and not is_password_strong(pwd, strict=True):
QMessageBox.warning(self, "密码强度不足",
"密码必须至少16位,包含大小写字母、数字和特殊字符\n\n"
"或切换到「低强度」模式(不推荐)")
return
self.custom_password = pwd
if DEBUG:
debug_log("密码验证通过")
if self.selected_key_mode == 'single':
if DEBUG:
debug_log("生成单独密钥对")
if self.selected_algorithm == 'rsa':
self.single_private, self.single_public = generate_rsa_key(4096)
else:
self.single_private, self.single_public = generate_x25519_keys()
if DEBUG:
debug_log("加密配置获取完成")
self.accept()
def get_config(self):
"""获取配置"""
return {
'algorithm': self.selected_algorithm,
'key_mode': self.selected_key_mode,
'use_password': self.use_password,
'strict_password': self.strict_password,
'custom_password': self.custom_password,
'single_private': self.single_private,
'single_public': self.single_public
}
# ==================== 主窗口 ====================
class MainWindow(QMainWindow):
"""主窗口类"""
def __init__(self):
super().__init__()
if DEBUG:
debug_log("初始化主窗口")
self.client = CloudClient()
self.tabs = QTabWidget()
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.on_tab_changed)
self.setCentralWidget(self.tabs)
self.setWindowTitle(f"云笔记 - 安全加密笔记")
self.resize(1200, 800)
self.setup_menu()
self.setup_statusbar()
self.editor_tabs = []
self.pending_encrypt_config = None
self.current_editor = None
ensure_global_keys()
self.check_login()
def setup_menu(self):
"""设置菜单栏"""
menubar = self.menuBar()
file_menu = menubar.addMenu("文件")
new_txt_action = QAction("新建文本文件(.txt)", self)
new_txt_action.setShortcut(QKeySequence("Ctrl+N"))
new_txt_action.triggered.connect(lambda: self.new_file('txt'))
file_menu.addAction(new_txt_action)
new_bjb_action = QAction("新建加密笔记(.bjb)...", self)
new_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
new_bjb_action.triggered.connect(lambda: self.new_file('bjb'))
file_menu.addAction(new_bjb_action)
file_menu.addSeparator()
open_action = QAction("打开文件...", self)
open_action.setShortcut(QKeySequence.Open)
open_action.triggered.connect(self.open_local_file_dialog)
file_menu.addAction(open_action)
open_cloud_action = QAction("从云端打开...", self)
open_cloud_action.setShortcut(QKeySequence("Ctrl+Shift+O"))
open_cloud_action.triggered.connect(self.open_cloud_file_dialog)
file_menu.addAction(open_cloud_action)
file_menu.addSeparator()
save_action = QAction("保存", self)
save_action.setShortcut(QKeySequence.Save)
save_action.triggered.connect(self.save_current_tab)
file_menu.addAction(save_action)
save_as_action = QAction("另存为...", self)
save_as_action.setShortcut(QKeySequence.SaveAs)
save_as_action.triggered.connect(self.save_as_current_tab)
file_menu.addAction(save_as_action)
file_menu.addSeparator()
exit_action = QAction("退出", self)
exit_action.setShortcut(QKeySequence.Quit)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 加密菜单
encrypt_menu = menubar.addMenu("加密")
encrypt_current_action = QAction("🔐 加密当前文件...", self)
encrypt_current_action.setShortcut(QKeySequence("Ctrl+E"))
encrypt_current_action.triggered.connect(self.encrypt_current_file)
encrypt_menu.addAction(encrypt_current_action)
encrypt_menu.addSeparator()
txt_to_bjb_action = QAction("📄 另存为加密笔记(.bjb)...", self)
txt_to_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+S"))
txt_to_bjb_action.triggered.connect(self.convert_txt_to_bjb)
encrypt_menu.addAction(txt_to_bjb_action)
cloud_menu = menubar.addMenu("云端")
manage_cloud_action = QAction("管理云端文件", self)
manage_cloud_action.triggered.connect(self.show_cloud_manager)
cloud_menu.addAction(manage_cloud_action)
upload_action = QAction("上传当前文件到云端", self)
upload_action.triggered.connect(self.upload_current_tab)
cloud_menu.addAction(upload_action)
cloud_menu.addSeparator()
logout_action = QAction("退出登录", self)
logout_action.triggered.connect(self.logout)
cloud_menu.addAction(logout_action)
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.triggered.connect(self.about)
help_menu.addAction(about_action)
def setup_statusbar(self):
"""设置状态栏"""
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.storage_label = QLabel("")
self.status_bar.addPermanentWidget(self.storage_label)
self.update_storage_info()
def update_storage_info(self):
"""更新存储信息"""
if self.client.token:
info = self.client.get_storage_info()
if info:
used_mb = info['total_size'] / (1024 * 1024)
max_mb = info['max_size'] / (1024 * 1024)
self.storage_label.setText(f"📁 {used_mb:.1f}MB / {max_mb:.0f}MB")
def check_login(self):
"""检查登录状态"""
settings = QSettings("CloudNote", "User")
token = settings.value("token")
user_id = settings.value("user_id")
username = settings.value("username")
if token and user_id and username:
self.client.set_auth(token, int(user_id), username)
user_info = self.client.get_user_info()
if user_info:
self.status_bar.showMessage(f"欢迎回来,{username}")
self.update_storage_info()
return
else:
settings.clear()
self.client.clear_auth()
dialog = LoginDialog(self.client, self)
if dialog.exec_() == QDialog.Accepted:
self.status_bar.showMessage(f"欢迎 {self.client.username}")
self.update_storage_info()
else:
sys.exit(0)
def logout(self):
"""退出登录"""
self.client.clear_auth()
settings = QSettings("CloudNote", "User")
settings.clear()
self.status_bar.showMessage("已退出登录")
while self.tabs.count() > 0:
self.close_tab(0)
self.check_login()
def new_file(self, file_type):
"""新建文件"""
if DEBUG:
debug_log(f"新建文件: {file_type}")
if file_type == 'txt':
tab = EditorTab(self.tabs, self.tabs.count(), file_type='txt', is_new=True, content="")
self.tabs.addTab(tab.text_edit, "未命名")
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
else:
# 先创建选项卡,再显示配置对话框
tab = EditorTab(self.tabs, self.tabs.count(), file_type='bjb', is_new=True, content="")
self.tabs.addTab(tab.text_edit, "未命名加密笔记")
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() == QDialog.Accepted:
config = dialog.get_config()
tab.encrypt_config = config
self.save_as_current_tab()
else:
self.close_tab(self.tabs.currentIndex())
def open_local_file_dialog(self):
"""打开本地文件对话框"""
if DEBUG:
debug_log("打开本地文件对话框")
file_path, _ = QFileDialog.getOpenFileName(
self, "打开文件", "", "笔记本文件 (*.bjb *.txt)"
)
if file_path:
if DEBUG:
debug_log(f"用户选择文件: {file_path}")
self.open_local_file(file_path)
def open_local_file(self, file_path, cloud_id=None):
"""
打开本地文件
修复:使用修复后的解密函数,支持单独密钥模式
"""
ext = os.path.splitext(file_path)[1].lower()
if DEBUG:
debug_log(f"打开本地文件: {file_path}, 类型: {ext}")
if ext == '.txt':
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
tab = EditorTab(self.tabs, self.tabs.count(),
file_path=file_path, cloud_id=cloud_id,
file_type='txt', content=content)
self.tabs.addTab(tab.text_edit, os.path.basename(file_path))
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
except Exception as e:
debug_error(f"读取文件失败: {e}")
QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}")
elif ext == '.bjb':
# 本地文件,is_cloud_file=False,会优先查找同目录.key文件
if DEBUG:
debug_log("调用解密函数(本地文件模式)")
content, success = decrypt_bjb_file(file_path, self, is_cloud_file=False)
if success and content is not None:
tab = EditorTab(self.tabs, self.tabs.count(),
file_path=file_path, cloud_id=cloud_id,
file_type='bjb', content=content)
self.tabs.addTab(tab.text_edit, os.path.basename(file_path))
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
else:
if DEBUG:
debug_log("解密失败,不创建选项卡")
def open_cloud_file_dialog(self):
"""打开云端文件对话框"""
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
self.check_login()
return
dialog = CloudFileTreeDialog(self.client, self)
dialog.exec_()
def save_current_tab(self):
"""保存当前选项卡 - 加密文件直接使用原有配置静默保存"""
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
if DEBUG:
debug_log(f"保存失败: 无效索引 {idx}")
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if DEBUG:
debug_log(f"保存当前选项卡: 索引 {idx}, 类型: {tab.file_type}, 新文件: {tab.is_new}")
debug_log(f"内容长度: {len(content)}字符")
# 新文件:调用另存为(会弹出配置对话框)
if tab.is_new:
if DEBUG:
debug_log("新文件,调用另存为")
self.save_as_current_tab()
return
# 已有文件路径
if tab.file_path and os.path.exists(tab.file_path):
try:
if tab.file_type == 'txt':
# 文本文件:直接写入
if DEBUG:
debug_log(f"保存文本文件: {tab.file_path}")
with open(tab.file_path, 'w', encoding='utf-8') as f:
f.write(content)
tab.mark_saved()
self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000)
if DEBUG:
debug_log("文本文件保存成功")
else: # bjb 加密文件
if DEBUG:
debug_log(f"保存加密文件: {tab.file_path}")
# 检查是否有加密配置
if hasattr(tab, 'encrypt_config') and tab.encrypt_config:
config = tab.encrypt_config
if DEBUG:
debug_log(f"使用现有加密配置 - 算法: {config.get('algorithm')}, 密钥模式: {config.get('key_mode')}, 密码保护: {config.get('use_password', False)}")
# 使用原有配置加密(静默保存,不弹窗)
encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content(
content,
config.get('algorithm', 'rsa'),
config.get('key_mode', 'global'),
config.get('custom_password'),
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(tab.file_path, 'wb') as f:
f.write(encrypted_data)
tab.mark_saved()
self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000)
if DEBUG:
debug_log("加密文件保存成功(使用现有配置,静默保存)")
else:
# 没有加密配置(理论上不应该发生,但作为后备)
if DEBUG:
debug_log("加密配置缺失,弹出配置对话框")
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() == QDialog.Accepted:
config = dialog.get_config()
tab.encrypt_config = config
encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content(
content, config['algorithm'], config['key_mode'],
config.get('custom_password'),
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(tab.file_path, 'wb') as f:
f.write(encrypted_data)
tab.mark_saved()
self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000)
if DEBUG:
debug_log("加密文件保存成功(新配置)")
else:
if DEBUG:
debug_log("用户取消保存")
return
except Exception as e:
debug_error(f"保存失败: {e}")
QMessageBox.critical(self, "保存失败", str(e))
else:
# 文件路径无效,调用另存为
if DEBUG:
debug_log("文件路径无效,调用另存为")
self.save_as_current_tab()
def save_as_current_tab(self):
"""另存为当前选项卡 - 首次保存时弹出配置对话框"""
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
if DEBUG:
debug_log(f"另存为失败: 无效索引 {idx}")
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if DEBUG:
debug_log(f"另存为当前选项卡: 索引 {idx}, 类型: {tab.file_type}")
debug_log(f"内容长度: {len(content)}字符")
if tab.file_type == 'txt':
# 文本文件另存为
if DEBUG:
debug_log("另存为文本文件")
file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)")
if not file_path:
if DEBUG:
debug_log("用户取消保存")
return
if not file_path.endswith('.txt'):
file_path += '.txt'
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
tab.file_path = file_path
tab.is_new = False
tab.mark_saved()
if DEBUG:
debug_log(f"文本文件另存为成功: {file_path}")
self.status_bar.showMessage(f"已保存: {os.path.basename(file_path)}", 3000)
except Exception as e:
debug_error(f"保存失败: {e}")
QMessageBox.critical(self, "保存失败", str(e))
else: # bjb 加密文件
if DEBUG:
debug_log("另存为加密文件")
# 获取加密配置
if hasattr(tab, 'encrypt_config') and tab.encrypt_config:
# 已有配置,直接使用(但另存为时应该让用户选择新位置)
config = tab.encrypt_config
if DEBUG:
debug_log("使用选项卡中的现有加密配置")
else:
# 没有配置,弹出配置对话框
if DEBUG:
debug_log("显示加密配置对话框")
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
if DEBUG:
debug_log("用户取消加密配置")
return
config = dialog.get_config()
tab.encrypt_config = config
if DEBUG:
debug_log("加密配置获取完成")
# 弹出保存文件对话框
save_dialog = SaveEncryptedFileDialog(
content, config['algorithm'], config['key_mode'],
config['use_password'], config.get('strict_password', True),
config.get('custom_password'),
config.get('single_private'), config.get('single_public'),
self
)
if save_dialog.exec_() == QDialog.Accepted:
tab.file_type = 'bjb'
tab.encrypt_config = config
tab.is_new = False
if save_dialog.selected_file_path:
tab.file_path = save_dialog.selected_file_path
tab.mark_saved()
if DEBUG:
debug_log(f"加密文件另存为成功: {tab.file_path}")
def encrypt_current_file(self):
"""加密当前文件"""
if DEBUG:
debug_log("加密当前文件功能被调用")
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
QMessageBox.warning(self, "提示", "没有打开的文件")
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if not content.strip():
ret = QMessageBox.question(self, "确认", "文件内容为空,是否继续加密?",
QMessageBox.Yes | QMessageBox.No)
if ret != QMessageBox.Yes:
return
dialog = EncryptCurrentFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
default_name = "encrypted_note.bjb"
if tab.file_path:
base_name = os.path.splitext(os.path.basename(tab.file_path))[0]
default_name = f"{base_name}_encrypted.bjb"
file_path, _ = QFileDialog.getSaveFileName(self, "保存加密文件", default_name, "加密笔记 (*.bjb)")
if not file_path:
return
if not file_path.endswith('.bjb'):
file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
content, config['algorithm'], config['key_mode'],
config['custom_password'],
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
if config['key_mode'] == 'single' and priv:
key_path = file_path + '.key'
with open(key_path, 'wb') as f:
f.write(priv)
QMessageBox.information(self, "加密成功",
f"文件已加密保存为:\n{file_path}\n\n"
f"密钥文件已保存为:\n{key_path}\n\n"
f"请妥善保管密钥文件!")
else:
QMessageBox.information(self, "加密成功",
f"文件已加密保存为:\n{file_path}\n\n"
f"使用全局密钥加密,无需额外保存密钥文件。")
ret = QMessageBox.question(self, "打开文件",
"是否在新选项卡中打开加密后的文件?",
QMessageBox.Yes | QMessageBox.No)
if ret == QMessageBox.Yes:
self.open_local_file(file_path)
except Exception as e:
debug_error(f"加密失败: {e}")
QMessageBox.critical(self, "加密失败", str(e))
def convert_txt_to_bjb(self):
"""将当前文本文件转换为加密笔记"""
if DEBUG:
debug_log("TXT转BJB功能被调用")
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
QMessageBox.warning(self, "提示", "没有打开的文件")
return
tab = self.editor_tabs[idx]
if tab.file_type == 'bjb':
QMessageBox.information(self, "提示", "当前文件已经是加密笔记格式")
return
content = tab.get_content()
dialog = EncryptCurrentFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
default_name = os.path.splitext(os.path.basename(tab.file_path))[0] + ".bjb" if tab.file_path else "converted.bjb"
file_path, _ = QFileDialog.getSaveFileName(self, "保存为加密笔记", default_name, "加密笔记 (*.bjb)")
if not file_path:
return
if not file_path.endswith('.bjb'):
file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
content, config['algorithm'], config['key_mode'],
config['custom_password'],
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
if config['key_mode'] == 'single' and priv:
key_path = file_path + '.key'
with open(key_path, 'wb') as f:
f.write(priv)
QMessageBox.information(self, "转换成功",
f"文件已转换为加密笔记:\n{file_path}\n\n"
f"密钥文件已保存为:\n{key_path}\n\n"
f"请妥善保管密钥文件!")
else:
QMessageBox.information(self, "转换成功",
f"文件已转换为加密笔记:\n{file_path}\n\n"
f"使用全局密钥加密,无需额外保存密钥文件。")
ret = QMessageBox.question(self, "打开文件",
"是否在新选项卡中打开加密后的文件?",
QMessageBox.Yes | QMessageBox.No)
if ret == QMessageBox.Yes:
self.open_local_file(file_path)
except Exception as e:
debug_error(f"转换失败: {e}")
QMessageBox.critical(self, "转换失败", str(e))
def upload_current_tab(self):
"""上传当前文件到云端"""
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
self.check_login()
return
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
return
tab = self.editor_tabs[idx]
if tab.is_new or not tab.file_path:
QMessageBox.warning(self, "提示", "请先保存文件到本地")
return
if tab.modified:
ret = QMessageBox.question(self, "提示", "文件未保存,是否先保存?", QMessageBox.Yes | QMessageBox.No)
if ret == QMessageBox.Yes:
self.save_current_tab()
folder, ok = QInputDialog.getText(self, "上传到云端", "请输入云端目录路径 (默认为 /):", text="/")
if not ok:
return
if not folder:
folder = "/"
res = self.client.upload_file(tab.file_path, folder)
if res:
QMessageBox.information(self, "成功", "文件已上传到云端")
else:
QMessageBox.warning(self, "错误", "上传失败")
def show_cloud_manager(self):
"""显示云端管理器"""
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
self.check_login()
return
dialog = CloudFileTreeDialog(self.client, self)
dialog.exec_()
def on_tab_changed(self, index):
"""当前选项卡改变时的回调"""
if index >= 0 and index < len(self.editor_tabs):
self.current_editor = self.editor_tabs[index]
else:
self.current_editor = None
def close_tab(self, index):
"""关闭指定索引的选项卡"""
if index < 0 or index >= len(self.editor_tabs):
return
tab = self.editor_tabs[index]
if tab.modified:
ret = QMessageBox.question(self, "提示", "文件已修改,是否保存?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if ret == QMessageBox.Yes:
self.save_current_tab()
elif ret == QMessageBox.Cancel:
return
self.tabs.removeTab(index)
self.editor_tabs.pop(index)
for i, t in enumerate(self.editor_tabs):
t.index = i
current_idx = self.tabs.currentIndex()
if current_idx >= 0 and current_idx < len(self.editor_tabs):
self.current_editor = self.editor_tabs[current_idx]
else:
self.current_editor = None
def about(self):
"""关于对话框"""
QMessageBox.about(self, "关于云笔记",
"云笔记 v3.0\n\n"
"功能特性:\n"
"• 支持 .txt 文本文件\n"
"• 支持 .bjb 加密笔记\n"
"• 云端同步功能\n"
"• 多选项卡编辑\n"
"• 加密当前文件\n"
"• TXT转BJB加密\n"
"• 支持高强度/低强度密码模式\n"
"• 端到端加密:服务器不存储私钥\n\n"
"加密模式:\n"
"1. RSA-4096 + AES-256-GCM\n"
"2. X25519 + AES-256-GCM\n\n"
"密钥方式:\n"
"• 默认加密:全局密钥对\n"
"• 安全加密:单独生成密钥对\n\n"
"快捷键:\n"
"• Ctrl+N: 新建文本\n"
"• Ctrl+Shift+N: 新建加密笔记\n"
"• Ctrl+E: 加密当前文件\n"
"• Ctrl+Shift+S: 另存为加密笔记\n"
"• Ctrl+S: 保存\n"
"• Ctrl+O: 打开文件\n\n"
f"密钥存储位置:\n"
f"• 用户目录: {USER_KEY_DIR}\n"
f"• 程序目录: {APP_KEY_DIR}")
def main():
"""主函数"""
if DEBUG:
debug_log("=" * 50)
debug_log("云笔记应用程序启动")
debug_log("=" * 50)
app = QApplication(sys.argv)
app.setStyle('Fusion')
ensure_global_keys()
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()