commit d2fc416283ff6fd8b1edb78fd5306f43da9e8d52 Author: zjing <1052308357@qq.com> Date: Sun Feb 1 17:41:32 2026 +0800 first commit diff --git a/.qtcreator/pyproject.toml.user b/.qtcreator/pyproject.toml.user new file mode 100644 index 0000000..5a508a6 --- /dev/null +++ b/.qtcreator/pyproject.toml.user @@ -0,0 +1,342 @@ + + + + + + EnvironmentId + {d278bd62-883b-4816-a712-738287b946d3} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 2 + true + + + + true + + 0 + + + + ProjectExplorer.Project.Target.0 + + Desktop + true + Python 3.14.2 + Python 3.14.2 + {bf68bfb7-0daf-4abe-a8f9-98ada8c9b297} + 0 + 0 + 0 + + /home/smart/qtpj/diskmananger/.qtcreator/Python_3_14_2venv + + + true + Python.PysideBuildStep + /home/smart/qtpj/diskmananger/.qtcreator/Python_3_14_2venv/bin/pyside6-project + /home/smart/qtpj/diskmananger/.qtcreator/Python_3_14_2venv/bin/pyside6-uic + + 1 + 构建 + 构建 + ProjectExplorer.BuildSteps.Build + + + 0 + 清除 + 清除 + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Python 3.14.2 Virtual Environment + Python.PySideBuildConfiguration + 0 + 0 + + + 0 + 部署 + 部署 + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + mainwindow.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/mainwindow.py + true + + /home/smart/qtpj/diskmananger/mainwindow.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + system_info.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/system_info.py + true + + /home/smart/qtpj/diskmananger/system_info.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + logger_config.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/logger_config.py + true + + /home/smart/qtpj/diskmananger/logger_config.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + disk_operations.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/disk_operations.py + true + + /home/smart/qtpj/diskmananger/disk_operations.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + 4 + /home/smart/qtpj/diskmananger/.qtcreator/Python_3_14_2venv/bin/python + /home/smart/qtpj/diskmananger/.qtcreator/Python_3_14_2venv + + 1 + + + 0 + 部署 + 部署 + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + mainwindow.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/mainwindow.py + true + + /home/smart/qtpj/diskmananger/mainwindow.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + system_info.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/system_info.py + true + + /home/smart/qtpj/diskmananger/system_info.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + logger_config.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/logger_config.py + true + + /home/smart/qtpj/diskmananger/logger_config.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + disk_operations.py + PythonEditor.RunConfiguration. + /home/smart/qtpj/diskmananger/disk_operations.py + true + + /home/smart/qtpj/diskmananger/disk_operations.py + true + true + /home/smart/qtpj/diskmananger + :0.0 + + 4 + + + + ProjectExplorer.Project.TargetCount + 1 + + + Version + 22 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/disk_operations.cpython-314.pyc b/__pycache__/disk_operations.cpython-314.pyc new file mode 100644 index 0000000..a66175c Binary files /dev/null and b/__pycache__/disk_operations.cpython-314.pyc differ diff --git a/__pycache__/logger_config.cpython-314.pyc b/__pycache__/logger_config.cpython-314.pyc new file mode 100644 index 0000000..ae64285 Binary files /dev/null and b/__pycache__/logger_config.cpython-314.pyc differ diff --git a/__pycache__/system_info.cpython-314.pyc b/__pycache__/system_info.cpython-314.pyc new file mode 100644 index 0000000..f0fc55b Binary files /dev/null and b/__pycache__/system_info.cpython-314.pyc differ diff --git a/__pycache__/ui_form.cpython-314.pyc b/__pycache__/ui_form.cpython-314.pyc new file mode 100644 index 0000000..eb3f9a8 Binary files /dev/null and b/__pycache__/ui_form.cpython-314.pyc differ diff --git a/disk_operations.py b/disk_operations.py new file mode 100644 index 0000000..929aac1 --- /dev/null +++ b/disk_operations.py @@ -0,0 +1,237 @@ +# disk_operations.py +import os +import logging +from PySide6.QtWidgets import QMessageBox, QInputDialog +from system_info import SystemInfoManager # 引入 SystemInfoManager 来复用 _run_command + +logger = logging.getLogger(__name__) # 获取当前模块的 logger 实例 + +class DiskOperations: + def __init__(self): + self.system_manager = SystemInfoManager() # 复用 SystemInfoManager 的命令执行器 + + def _get_device_path(self, device_name): + """ + 辅助函数:将设备名(如 'sda1')转换为完整路径(如 '/dev/sda1')。 + """ + if not device_name.startswith('/dev/'): + return f'/dev/{device_name}' + return device_name + + def mount_partition(self, device_name, mount_point=None): + """ + 挂载指定的分区。 + 如果未提供挂载点,会尝试查找现有挂载点或提示用户输入。 + """ + dev_path = self._get_device_path(device_name) + logger.info(f"尝试挂载设备: {dev_path}") + + if not mount_point: + # 尝试从系统信息中获取当前挂载点 + devices = self.system_manager.get_block_devices() + found_mount_point = None + for dev in devices: + if dev.get('name') == device_name: + found_mount_point = dev.get('mountpoint') + break + if 'children' in dev: + for child in dev['children']: + if child.get('name') == device_name: + found_mount_point = child.get('mountpoint') + break + if found_mount_point: + break + + if found_mount_point and found_mount_point != '[SWAP]' and found_mount_point != '': + # 如果设备已经有挂载点,并且不是SWAP,则直接使用 + mount_point = found_mount_point + logger.info(f"设备 {dev_path} 已经挂载到 {mount_point}。") + QMessageBox.information(None, "信息", f"设备 {dev_path} 已经挂载到 {mount_point}。") + return True # 已经挂载,视为成功 + else: + # 如果没有挂载点,或者挂载点是SWAP,则提示用户输入 + mount_point, ok = QInputDialog.getText(None, "挂载分区", + f"请输入 {dev_path} 的挂载点 (例如: /mnt/data):", + text=f"/mnt/{device_name}") + if not ok or not mount_point: + logger.info("用户取消了挂载操作或未提供挂载点。") + return False + + # 确保挂载点目录存在 + if not os.path.exists(mount_point): + try: + os.makedirs(mount_point, exist_ok=True) + logger.info(f"创建挂载点目录: {mount_point}") + except OSError as e: + logger.error(f"创建挂载点目录失败 {mount_point}: {e}") + QMessageBox.critical(None, "错误", f"创建挂载点目录失败: {e}") + return False + + try: + stdout, stderr = self.system_manager._run_command(["mount", dev_path, mount_point], root_privilege=True) + logger.info(f"成功挂载 {dev_path} 到 {mount_point}") + QMessageBox.information(None, "成功", f"成功挂载 {dev_path} 到 {mount_point}") + return True + except Exception as e: + logger.error(f"挂载 {dev_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"挂载 {dev_path} 失败: {e}") + return False + + def unmount_partition(self, device_name): + """ + 卸载指定的分区。 + """ + dev_path = self._get_device_path(device_name) + logger.info(f"尝试卸载设备: {dev_path}") + + # 尝试从系统信息中获取当前挂载点 + current_mount_point = None + devices = self.system_manager.get_block_devices() + for dev in devices: + if dev.get('name') == device_name: + current_mount_point = dev.get('mountpoint') + break + if 'children' in dev: + for child in dev['children']: + if child.get('name') == device_name: + current_mount_point = child.get('mountpoint') + break + if current_mount_point: + break + + if not current_mount_point or current_mount_point == '[SWAP]' or current_mount_point == '': + logger.warning(f"设备 {dev_path} 未挂载或无法确定挂载点,无法卸载。") + QMessageBox.warning(None, "警告", f"设备 {dev_path} 未挂载或无法确定挂载点,无法卸载。") + return False + + try: + # 尝试通过挂载点卸载,通常更可靠 + stdout, stderr = self.system_manager._run_command(["umount", current_mount_point], root_privilege=True) + logger.info(f"成功卸载 {current_mount_point} ({dev_path})") + QMessageBox.information(None, "成功", f"成功卸载 {current_mount_point} ({dev_path})") + return True + except Exception as e: + logger.error(f"卸载 {current_mount_point} ({dev_path}) 失败: {e}") + QMessageBox.critical(None, "错误", f"卸载 {current_mount_point} ({dev_path}) 失败: {e}") + return False + + def delete_partition(self, device_name): + """ + 删除指定的分区。 + 此操作不可逆,会丢失数据。 + """ + dev_path = self._get_device_path(device_name) + logger.info(f"尝试删除分区: {dev_path}") + + # 安全检查: 确保是分区,而不是整个磁盘 + # 简单的检查方法是看设备名是否包含数字 (如 sda1, nvme0n1p1) + if not any(char.isdigit() for char in device_name): + QMessageBox.warning(None, "警告", f"{dev_path} 看起来不是一个分区。为安全起见,不执行删除操作。") + logger.warning(f"尝试删除整个磁盘 {dev_path},已阻止。") + return False + + reply = QMessageBox.question(None, "确认删除分区", + f"你确定要删除分区 {dev_path} 吗?\n" + "此操作不可逆,将导致数据丢失!", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + logger.info(f"用户取消了删除分区 {dev_path} 的操作。") + return False + + # 首先尝试卸载分区(如果已挂载) + try: + # umount 如果设备未挂载会返回非零退出码,所以这里 check_output=False + self.system_manager._run_command(["umount", dev_path], root_privilege=True, check_output=False) + logger.info(f"尝试卸载 {dev_path} (如果已挂载)。") + except Exception as e: + logger.warning(f"卸载 {dev_path} 失败或未挂载: {e}") + # 继续执行,因为即使卸载失败也可能能删除 + + # 使用 parted 来删除分区 + # parted 需要父磁盘设备名和分区号 + # 例如,对于 /dev/sda1,需要 /dev/sda 和分区号 1 + partition_number_str = ''.join(filter(str.isdigit, device_name)) # 从 sda1 提取 "1" + if not partition_number_str: + QMessageBox.critical(None, "错误", f"无法从 {device_name} 解析分区号。") + logger.error(f"无法从 {device_name} 解析分区号。") + return False + + parent_disk_name = device_name.rstrip(partition_number_str) # 从 sda1 提取 "sda" + parent_disk_path = self._get_device_path(parent_disk_name) + + try: + # parted -s /dev/sda rm 1 + stdout, stderr = self.system_manager._run_command( + ["parted", "-s", parent_disk_path, "rm", partition_number_str], + root_privilege=True + ) + logger.info(f"成功删除分区 {dev_path}") + QMessageBox.information(None, "成功", f"成功删除分区 {dev_path}") + return True + except Exception as e: + logger.error(f"删除分区 {dev_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"删除分区 {dev_path} 失败: {e}") + return False + + def format_partition(self, device_name, fstype=None): + """ + 格式化指定的分区。 + 此操作不可逆,会丢失数据。 + """ + dev_path = self._get_device_path(device_name) + logger.info(f"尝试格式化分区: {dev_path}") + + # 安全检查: 确保是分区 + if not any(char.isdigit() for char in device_name): + QMessageBox.warning(None, "警告", f"{dev_path} 看起来不是一个分区。为安全起见,不执行格式化操作。") + logger.warning(f"尝试格式化整个磁盘 {dev_path},已阻止。") + return False + + if not fstype: + # 提示用户选择文件系统类型 + fstypes = ["ext4", "xfs", "fat32", "ntfs"] # 常用文件系统 + fstype, ok = QInputDialog.getItem(None, "格式化分区", + f"请选择 {dev_path} 的文件系统类型:", + fstypes, 0, False) # 默认选择 ext4 + if not ok or not fstype: + logger.info("用户取消了格式化操作或未选择文件系统。") + return False + + reply = QMessageBox.question(None, "确认格式化分区", + f"你确定要格式化分区 {dev_path} 为 {fstype} 吗?\n" + "此操作不可逆,将导致数据丢失!", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + logger.info(f"用户取消了格式化分区 {dev_path} 的操作。") + return False + + # 首先尝试卸载分区(如果已挂载) + try: + self.system_manager._run_command(["umount", dev_path], root_privilege=True, check_output=False) + logger.info(f"尝试卸载 {dev_path} (如果已挂载)。") + except Exception as e: + logger.warning(f"卸载 {dev_path} 失败或未挂载: {e}") + + # 执行 mkfs 命令 + try: + mkfs_cmd = [] + if fstype == "ext4": + mkfs_cmd = ["mkfs.ext4", "-F", dev_path] # -F 强制执行 + elif fstype == "xfs": + mkfs_cmd = ["mkfs.xfs", "-f", dev_path] # -f 强制执行 + elif fstype == "fat32": + mkfs_cmd = ["mkfs.fat", "-F", "32", dev_path] + elif fstype == "ntfs": + mkfs_cmd = ["mkfs.ntfs", "-f", dev_path] # -f 强制执行 + else: + raise ValueError(f"不支持的文件系统类型: {fstype}") + + stdout, stderr = self.system_manager._run_command(mkfs_cmd, root_privilege=True) + logger.info(f"成功格式化分区 {dev_path} 为 {fstype}") + QMessageBox.information(None, "成功", f"成功格式化分区 {dev_path} 为 {fstype}") + return True + except Exception as e: + logger.error(f"格式化分区 {dev_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"格式化分区 {dev_path} 失败: {e}") + return False + diff --git a/form.ui b/form.ui new file mode 100644 index 0000000..cc653e4 --- /dev/null +++ b/form.ui @@ -0,0 +1,141 @@ + + + MainWindow + + + + 0 + 0 + 1000 + 700 + + + + Linux 存储管理工具 + + + + + + + 0 + + + + 块设备概览 + + + + + + + 设备名 + + + + + 类型 + + + + + 大小 + + + + + 挂载点 + + + + + 文件系统 + + + + + 只读 + + + + + UUID + + + + + PARTUUID + + + + + 厂商 + + + + + 型号 + + + + + 序列号 + + + + + 主次号 + + + + + 父设备名 + + + + + + + + + RAID 管理 + + + + + LVM 管理 + + + + + + + + 刷新数据 + + + + + + + true + + + + + + + + + 0 + 0 + 1000 + 22 + + + + + + + + diff --git a/logger_config.py b/logger_config.py new file mode 100644 index 0000000..07474ea --- /dev/null +++ b/logger_config.py @@ -0,0 +1,54 @@ +# logger_config.py +import logging +from PySide6.QtWidgets import QTextEdit +from PySide6.QtCore import Signal, QObject + +class QTextEditLogger(logging.Handler, QObject): + """ + 自定义的 logging 处理器,将日志消息发送到 QTextEdit 控件。 + """ + # 定义一个信号,用于在 GUI 线程中更新 QTextEdit + append_text = Signal(str) + + def __init__(self, widget: QTextEdit): + super().__init__() + QObject.__init__(self) # 初始化 QObject 部分 + self.widget = widget + self.widget.setReadOnly(True) # 确保日志区域是只读的 + # 连接信号到 QTextEdit 的 append 方法 + self.append_text.connect(self.widget.append) + + def emit(self, record): + """ + 处理日志记录,将其格式化并通过信号发送到 QTextEdit。 + """ + msg = self.format(record) + self.append_text.emit(msg) + +def setup_logging(text_edit_widget: QTextEdit): + """ + 配置全局 logging,使其输出到控制台和指定的 QTextEdit 控件。 + """ + root_logger = logging.getLogger() + # 设置日志级别,可以根据需要调整 (DEBUG, INFO, WARNING, ERROR, CRITICAL) + root_logger.setLevel(logging.INFO) + + # 清除现有的处理器,防止重复输出(如果多次调用此函数) + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 1. 控制台处理器 (可选,用于调试) + console_handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # 2. QTextEdit 处理器 + text_edit_handler = QTextEditLogger(text_edit_widget) + text_edit_handler.setFormatter(formatter) + root_logger.addHandler(text_edit_handler) + + return root_logger + +# 获取一个全局的 logger 实例,方便其他模块使用 +logger = logging.getLogger(__name__) diff --git a/mainwindow.py b/mainwindow.py new file mode 100644 index 0000000..80d1d32 --- /dev/null +++ b/mainwindow.py @@ -0,0 +1,189 @@ +# mainwindow.py +import sys +import logging # 导入 logging 模块 + +from PySide6.QtWidgets import (QApplication, QMainWindow, QTreeWidgetItem, + QMessageBox, QHeaderView, QMenu, QInputDialog) +from PySide6.QtCore import Qt, QPoint + +# 导入自动生成的 UI 文件 +from ui_form import Ui_MainWindow +# 导入我们自己编写的系统信息管理模块 +from system_info import SystemInfoManager +# 导入日志配置 +from logger_config import setup_logging, logger # 导入日志配置函数和 logger 实例 +# 导入磁盘操作模块 +from disk_operations import DiskOperations + +class MainWindow(QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + # 设置日志输出到 QTextEdit + setup_logging(self.ui.logOutputTextEdit) + logger.info("应用程序启动。") + + # 初始化系统信息管理器和磁盘操作管理器 + self.system_manager = SystemInfoManager() + self.disk_ops = DiskOperations() + + # 连接刷新按钮的信号到槽函数 + if hasattr(self.ui, 'refreshButton'): + self.ui.refreshButton.clicked.connect(self.refresh_block_devices_info) + else: + logger.warning("Warning: refreshButton not found in UI. Please add it in form.ui and regenerate ui_form.py.") + + # 启用 treeWidget 的自定义上下文菜单 + self.ui.treeWidget_block_devices.setContextMenuPolicy(Qt.CustomContextMenu) + self.ui.treeWidget_block_devices.customContextMenuRequested.connect(self.show_block_device_context_menu) + + # 初始化时刷新一次数据 + self.refresh_block_devices_info() + logger.info("块设备信息已初始化加载。") + + def refresh_block_devices_info(self): + """ + 刷新块设备信息并显示在 QTreeWidget 中。 + """ + self.ui.treeWidget_block_devices.clear() # 清空现有内容 + + # 定义所有要显示的列头和对应的 lsblk 字段名 + columns = [ + ("设备名", 'name'), + ("类型", 'type'), + ("大小", 'size'), + ("挂载点", 'mountpoint'), + ("文件系统", 'fstype'), + ("只读", 'ro'), + ("UUID", 'uuid'), + ("PARTUUID", 'partuuid'), + ("厂商", 'vendor'), + ("型号", 'model'), + ("序列号", 'serial'), + ("主次号", 'maj:min'), + ("父设备名", 'pkname'), + ] + + headers = [col[0] for col in columns] + self.field_keys = [col[1] for col in columns] # 保存字段键,供后续使用 + + self.ui.treeWidget_block_devices.setColumnCount(len(headers)) # 确保列数正确 + self.ui.treeWidget_block_devices.setHeaderLabels(headers) + + # 调整列宽以适应内容 + for i in range(len(headers)): + self.ui.treeWidget_block_devices.header().setSectionResizeMode(i, QHeaderView.ResizeToContents) + + try: + devices = self.system_manager.get_block_devices() + for dev in devices: + self._add_device_to_tree(self.ui.treeWidget_block_devices, dev) + + # 自动调整列宽 + for i in range(len(headers)): + self.ui.treeWidget_block_devices.resizeColumnToContents(i) + logger.info("块设备信息刷新成功。") + + except Exception as e: + QMessageBox.critical(self, "错误", f"刷新块设备信息失败: {e}") + logger.error(f"刷新块设备信息失败: {e}") + + def _add_device_to_tree(self, parent_item, dev_data): + """ + 辅助函数,将单个设备及其子设备添加到 QTreeWidget。 + parent_item 可以是 QTreeWidget 本身,也可以是另一个 QTreeWidgetItem。 + """ + item = QTreeWidgetItem(parent_item) + for i, key in enumerate(self.field_keys): + value = dev_data.get(key) + if key == 'ro': # 特殊处理布尔值 + item.setText(i, "是" if value else "否") + elif value is None: # None 值显示为空字符串 + item.setText(i, "") + else: + item.setText(i, str(value)) + + # 将原始设备数据存储在 item 的 data 属性中,方便后续操作时获取 + item.setData(0, Qt.UserRole, dev_data) + + # 如果有子设备(分区),也显示出来 + if 'children' in dev_data: + for child in dev_data['children']: + self._add_device_to_tree(item, child) + item.setExpanded(True) # 默认展开父节点,以便看到分区 + + def show_block_device_context_menu(self, pos: QPoint): + """ + 显示块设备列表的右键上下文菜单。 + """ + item = self.ui.treeWidget_block_devices.itemAt(pos) + if item: + dev_data = item.data(0, Qt.UserRole) # 获取存储的原始设备数据 + if not dev_data: + logger.warning(f"无法获取设备 {item.text(0)} 的详细数据。") + return + + device_name = dev_data.get('name') + device_type = dev_data.get('type') + mount_point = dev_data.get('mountpoint') + + menu = QMenu(self) + + # 挂载/卸载操作 + # 只有 'part' (分区) 和 'disk' (整个磁盘,但通常只挂载分区) 可以被挂载/卸载 + if device_type in ['part', 'disk']: + if not mount_point or mount_point == '' or mount_point == 'N/A': # 未挂载 + mount_action = menu.addAction(f"挂载 {device_name}...") + mount_action.triggered.connect(lambda: self._handle_mount(device_name)) + elif mount_point != '[SWAP]': # 已挂载且不是SWAP + unmount_action = menu.addAction(f"卸载 {device_name}") + unmount_action.triggered.connect(lambda: self._handle_unmount(device_name)) + + # 分隔符,用于区分操作 + if menu.actions(): + menu.addSeparator() + + # 删除分区和格式化操作 + # 这些操作通常只针对 'part' (分区) + if device_type == 'part': + delete_action = menu.addAction(f"删除分区 {device_name}") + delete_action.triggered.connect(lambda: self._handle_delete_partition(device_name)) + + format_action = menu.addAction(f"格式化分区 {device_name}...") + format_action.triggered.connect(lambda: self._handle_format_partition(device_name)) + + if menu.actions(): # 只有当菜单中有动作时才显示 + menu.exec(self.ui.treeWidget_block_devices.mapToGlobal(pos)) + else: + logger.info(f"设备 {device_name} 没有可用的操作。") + else: + logger.info("右键点击了空白区域。") + + def _handle_mount(self, device_name): + """处理挂载操作,并刷新UI。""" + if self.disk_ops.mount_partition(device_name): + self.refresh_block_devices_info() # 操作成功后刷新UI + + def _handle_unmount(self, device_name): + """处理卸载操作,并刷新UI。""" + if self.disk_ops.unmount_partition(device_name): + self.refresh_block_devices_info() # 操作成功后刷新UI + + def _handle_delete_partition(self, device_name): + """处理删除分区操作,并刷新UI。""" + if self.disk_ops.delete_partition(device_name): + self.refresh_block_devices_info() # 操作成功后刷新UI + + def _handle_format_partition(self, device_name): + """处理格式化分区操作,并刷新UI。""" + if self.disk_ops.format_partition(device_name): + self.refresh_block_devices_info() # 操作成功后刷新UI + + +if __name__ == "__main__": + app = QApplication(sys.argv) + widget = MainWindow() + widget.show() + sys.exit(app.exec()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b9a0987 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "PySide Widgets Project" + +[tool.pyside6-project] +files = ["disk_operations.py", "form.ui", "logger_config.py", "mainwindow.py", "system_info.py"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7fa34f0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PySide6 diff --git a/system_info.py b/system_info.py new file mode 100644 index 0000000..a9bb265 --- /dev/null +++ b/system_info.py @@ -0,0 +1,147 @@ +# system_info.py +import subprocess +import json +import os +import logging # 导入 logging 模块 + +logger = logging.getLogger(__name__) # 获取当前模块的 logger 实例 + +class SystemInfoManager: + def __init__(self): + pass + + def _run_command(self, cmd, root_privilege=False, check_output=True): + """ + 执行shell命令并返回stdout和stderr。 + 如果需要root权限,会尝试使用sudo。 + 所有输出和错误都会通过 logger 记录。 + """ + full_cmd = [] + if root_privilege: + # 检查当前是否已经是root用户 + if os.geteuid() != 0: + full_cmd.append("sudo") + + full_cmd.extend(cmd) + cmd_str = ' '.join(full_cmd) # 用于日志记录的完整命令字符串 + + logger.info(f"执行命令: {cmd_str}") + + try: + # text=True 自动解码为字符串,encoding='utf-8' 确保正确处理中文 + # check=check_output 表示如果命令返回非零退出码,则抛出CalledProcessError + result = subprocess.run( + full_cmd, + capture_output=True, + text=True, + check=check_output, + encoding='utf-8', + # 设置LANG环境变量,确保命令输出使用UTF-8编码,避免解析问题 + env=dict(os.environ, LANG="en_US.UTF-8") + ) + if result.stdout: + logger.debug(f"命令输出 (stdout):\n{result.stdout.strip()}") + if result.stderr: + logger.warning(f"命令输出 (stderr):\n{result.stderr.strip()}") + return result.stdout.strip(), result.stderr.strip() + except subprocess.CalledProcessError as e: + # 捕获命令执行失败的情况 + error_msg = f"命令执行失败: {cmd_str}\n" \ + f"退出码: {e.returncode}\n" \ + f"标准输出: {e.stdout}\n" \ + f"标准错误: {e.stderr}" + logger.error(error_msg) # 使用 logger 记录错误 + raise ValueError(error_msg) # 抛出更易于处理的异常 + except FileNotFoundError: + error_msg = f"命令未找到: {full_cmd[0]}。请确保已安装。" + logger.error(error_msg) + raise FileNotFoundError(error_msg) + except Exception as e: + error_msg = f"执行命令时发生未知错误: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) + + def get_block_devices(self): + """ + 获取所有块设备的信息,以JSON格式返回。 + 使用 lsblk -J 命令。 + """ + try: + stdout, _ = self._run_command(["lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME"], root_privilege=False) + data = json.loads(stdout) + logger.info("成功获取块设备信息。") + return data.get('blockdevices', []) + except Exception as e: + logger.error(f"获取块设备信息失败: {e}") # 使用 logger 记录错误 + return [] + + def get_mdadm_arrays(self): + """ + 获取所有RAID阵列的详细信息。 + 使用 mdadm --detail --scan 命令。 + """ + try: + stdout, _ = self._run_command(["mdadm", "--detail", "--scan"], root_privilege=False) + logger.info("成功获取RAID阵列信息。") + return stdout + except Exception as e: + logger.error(f"获取RAID阵列信息失败: {e}") + return "无法获取RAID阵列信息。" + + def get_lvm_info(self): + """ + 获取LVM的物理卷、卷组、逻辑卷信息。 + 使用 pvs, vgs, lvs 命令,并尝试获取JSON格式。 + """ + lvm_info = {} + try: + stdout_pvs, _ = self._run_command(["pvs", "--reportformat", "json"], root_privilege=False) + lvm_info['pvs'] = json.loads(stdout_pvs).get('report', [])[0].get('pv', []) + logger.info("成功获取物理卷信息。") + except Exception as e: + logger.error(f"获取物理卷信息失败: {e}") + lvm_info['pvs'] = [] + + try: + stdout_vgs, _ = self._run_command(["vgs", "--reportformat", "json"], root_privilege=False) + lvm_info['vgs'] = json.loads(stdout_vgs).get('report', [])[0].get('vg', []) + logger.info("成功获取卷组信息。") + except Exception as e: + logger.error(f"获取卷组信息失败: {e}") + lvm_info['vgs'] = [] + + try: + stdout_lvs, _ = self._run_command(["lvs", "--reportformat", "json"], root_privilege=False) + lvm_info['lvs'] = json.loads(stdout_lvs).get('report', [])[0].get('lv', []) + logger.info("成功获取逻辑卷信息。") + except Exception as e: + logger.error(f"获取逻辑卷信息失败: {e}") + lvm_info['lvs'] = [] + + return lvm_info + +# 示例用法 (可以在此模块中添加测试代码) +if __name__ == "__main__": + # 为了在单独运行时也能看到日志输出,这里简单配置一下 + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + manager = SystemInfoManager() + logger.info("--- 块设备信息 ---") + devices = manager.get_block_devices() + for dev in devices: + logger.info(f" NAME: {dev.get('name')}, TYPE: {dev.get('type')}, SIZE: {dev.get('size')}, MOUNTPOINT: {dev.get('mountpoint')}") + + logger.info("\n--- RAID 阵列信息 ---") + raid_info = manager.get_mdadm_arrays() + logger.info(raid_info) + + logger.info("\n--- LVM 信息 ---") + lvm_info = manager.get_lvm_info() + logger.info("物理卷 (PVs):") + for pv in lvm_info['pvs']: + logger.info(f" {pv.get('pv_name')} (VG: {pv.get('vg_name')}, Size: {pv.get('pv_size')})") + logger.info("卷组 (VGs):") + for vg in lvm_info['vgs']: + logger.info(f" {vg.get('vg_name')} (Size: {vg.get('vg_size')}, PVs: {vg.get('pv_count')}, LVs: {vg.get('lv_count')})") + logger.info("逻辑卷 (LVs):") + for lv in lvm_info['lvs']: + logger.info(f" {lv.get('lv_name')} (VG: {lv.get('vg_name')}, Size: {lv.get('lv_size')})") diff --git a/ui_form.py b/ui_form.py new file mode 100644 index 0000000..dc228af --- /dev/null +++ b/ui_form.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'form.ui' +## +## Created by: Qt User Interface Compiler version 6.10.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHeaderView, QMainWindow, QMenuBar, + QPushButton, QSizePolicy, QStatusBar, QTabWidget, + QTextEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(1000, 700) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.verticalLayout = QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tabWidget = QTabWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + self.tab_block_devices = QWidget() + self.tab_block_devices.setObjectName(u"tab_block_devices") + self.verticalLayout_2 = QVBoxLayout(self.tab_block_devices) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.treeWidget_block_devices = QTreeWidget(self.tab_block_devices) + self.treeWidget_block_devices.setObjectName(u"treeWidget_block_devices") + + self.verticalLayout_2.addWidget(self.treeWidget_block_devices) + + self.tabWidget.addTab(self.tab_block_devices, "") + self.tab_raid = QWidget() + self.tab_raid.setObjectName(u"tab_raid") + self.tabWidget.addTab(self.tab_raid, "") + self.tab_lvm = QWidget() + self.tab_lvm.setObjectName(u"tab_lvm") + self.tabWidget.addTab(self.tab_lvm, "") + + self.verticalLayout.addWidget(self.tabWidget) + + self.refreshButton = QPushButton(self.centralwidget) + self.refreshButton.setObjectName(u"refreshButton") + + self.verticalLayout.addWidget(self.refreshButton) + + self.logOutputTextEdit = QTextEdit(self.centralwidget) + self.logOutputTextEdit.setObjectName(u"logOutputTextEdit") + self.logOutputTextEdit.setReadOnly(True) + + self.verticalLayout.addWidget(self.logOutputTextEdit) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 1000, 22)) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Linux \u5b58\u50a8\u7ba1\u7406\u5de5\u5177", None)) + ___qtreewidgetitem = self.treeWidget_block_devices.headerItem() + ___qtreewidgetitem.setText(12, QCoreApplication.translate("MainWindow", u"\u7236\u8bbe\u5907\u540d", None)); + ___qtreewidgetitem.setText(11, QCoreApplication.translate("MainWindow", u"\u4e3b\u6b21\u53f7", None)); + ___qtreewidgetitem.setText(10, QCoreApplication.translate("MainWindow", u"\u5e8f\u5217\u53f7", None)); + ___qtreewidgetitem.setText(9, QCoreApplication.translate("MainWindow", u"\u578b\u53f7", None)); + ___qtreewidgetitem.setText(8, QCoreApplication.translate("MainWindow", u"\u5382\u5546", None)); + ___qtreewidgetitem.setText(7, QCoreApplication.translate("MainWindow", u"PARTUUID", None)); + ___qtreewidgetitem.setText(6, QCoreApplication.translate("MainWindow", u"UUID", None)); + ___qtreewidgetitem.setText(5, QCoreApplication.translate("MainWindow", u"\u53ea\u8bfb", None)); + ___qtreewidgetitem.setText(4, QCoreApplication.translate("MainWindow", u"\u6587\u4ef6\u7cfb\u7edf", None)); + ___qtreewidgetitem.setText(3, QCoreApplication.translate("MainWindow", u"\u6302\u8f7d\u70b9", None)); + ___qtreewidgetitem.setText(2, QCoreApplication.translate("MainWindow", u"\u5927\u5c0f", None)); + ___qtreewidgetitem.setText(1, QCoreApplication.translate("MainWindow", u"\u7c7b\u578b", None)); + ___qtreewidgetitem.setText(0, QCoreApplication.translate("MainWindow", u"\u8bbe\u5907\u540d", None)); + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_block_devices), QCoreApplication.translate("MainWindow", u"\u5757\u8bbe\u5907\u6982\u89c8", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_raid), QCoreApplication.translate("MainWindow", u"RAID \u7ba1\u7406", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_lvm), QCoreApplication.translate("MainWindow", u"LVM \u7ba1\u7406", None)) + self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None)) + # retranslateUi +