374 lines
12 KiB
Python
374 lines
12 KiB
Python
"""
|
|
主窗口
|
|
"""
|
|
import os
|
|
from typing import Optional
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QPushButton, QStatusBar, QMessageBox,
|
|
QApplication, QSplitter, QFrame
|
|
)
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
|
from PyQt5.QtGui import QFont, QCloseEvent
|
|
|
|
from disk_manager import DiskManager, Partition
|
|
from file_scanner import FileScanner, FileItem, ScanProgress
|
|
from utils import format_size, check_sudo_needs_password, verify_sudo_password, safe_delete
|
|
|
|
from gui.partition_dialog import PartitionDialog
|
|
from gui.file_browser import FileBrowser
|
|
from gui.confirm_dialog import LogDeleteConfirmDialog
|
|
from gui.sudo_dialog import SudoDialog
|
|
|
|
|
|
class DirectorySizeThread(QThread):
|
|
"""目录大小统计线程"""
|
|
progress = pyqtSignal(object) # ScanProgress
|
|
finished = pyqtSignal(dict) # {path: size}
|
|
error = pyqtSignal(str)
|
|
|
|
def __init__(self, scanner: FileScanner, path: str):
|
|
super().__init__()
|
|
self.scanner = scanner
|
|
self.path = path
|
|
|
|
def run(self):
|
|
try:
|
|
sizes = self.scanner.scan_directory_sizes(
|
|
self.path,
|
|
callback=lambda p: self.progress.emit(p)
|
|
)
|
|
self.finished.emit(sizes)
|
|
except Exception as e:
|
|
self.error.emit(str(e))
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""主窗口"""
|
|
|
|
def __init__(self, sudo_password: Optional[str] = None):
|
|
super().__init__()
|
|
self.sudo_password = sudo_password
|
|
self.disk_manager = DiskManager(sudo_password)
|
|
self.file_scanner = FileScanner(sudo_password)
|
|
self.current_partition: Optional[Partition] = None
|
|
self.current_mount_point: Optional[str] = None
|
|
self.dir_sizes = {} # {path: size}
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""初始化界面"""
|
|
self.setWindowTitle("Linux 日志删除助手")
|
|
self.setMinimumSize(900, 600)
|
|
|
|
# 中央部件
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# 主布局
|
|
main_layout = QVBoxLayout(central_widget)
|
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
main_layout.setSpacing(10)
|
|
|
|
# 顶部信息栏
|
|
info_layout = QHBoxLayout()
|
|
|
|
# 分区信息
|
|
self.partition_label = QLabel("当前分区:未选择")
|
|
font = QFont()
|
|
font.setPointSize(11)
|
|
self.partition_label.setFont(font)
|
|
info_layout.addWidget(self.partition_label)
|
|
|
|
info_layout.addStretch()
|
|
|
|
# 切换分区按钮
|
|
self.switch_partition_button = QPushButton("🔄 切换分区")
|
|
self.switch_partition_button.clicked.connect(self.switch_partition)
|
|
info_layout.addWidget(self.switch_partition_button)
|
|
|
|
main_layout.addLayout(info_layout)
|
|
|
|
# 工具栏
|
|
toolbar_layout = QHBoxLayout()
|
|
|
|
# 刷新按钮
|
|
self.refresh_button = QPushButton("🔄 刷新")
|
|
self.refresh_button.clicked.connect(self.refresh)
|
|
toolbar_layout.addWidget(self.refresh_button)
|
|
|
|
# 统计按钮
|
|
self.scan_button = QPushButton("📊 统计目录大小")
|
|
self.scan_button.clicked.connect(self.scan_directory_sizes)
|
|
toolbar_layout.addWidget(self.scan_button)
|
|
|
|
# 快捷删除日志按钮
|
|
self.delete_logs_button = QPushButton("🗑️ 快捷删除系统日志")
|
|
self.delete_logs_button.setStyleSheet(
|
|
"QPushButton { background-color: #e74c3c; color: white; font-weight: bold; }"
|
|
)
|
|
self.delete_logs_button.clicked.connect(self.quick_delete_logs)
|
|
self.delete_logs_button.hide() # 默认隐藏,检测到大日志时显示
|
|
toolbar_layout.addWidget(self.delete_logs_button)
|
|
|
|
toolbar_layout.addStretch()
|
|
|
|
main_layout.addLayout(toolbar_layout)
|
|
|
|
# 分隔线
|
|
separator = QFrame()
|
|
separator.setFrameStyle(QFrame.HLine | QFrame.Sunken)
|
|
main_layout.addWidget(separator)
|
|
|
|
# 文件浏览器
|
|
self.file_browser = FileBrowser(
|
|
self.file_scanner,
|
|
self.sudo_password,
|
|
self
|
|
)
|
|
self.file_browser.directory_changed.connect(self.on_directory_changed)
|
|
self.file_browser.files_deleted.connect(self.on_files_deleted)
|
|
main_layout.addWidget(self.file_browser)
|
|
|
|
# 状态栏
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self.status_bar.showMessage("就绪")
|
|
|
|
# 进度条(用于扫描)
|
|
self.progress_bar = QLabel()
|
|
self.status_bar.addPermanentWidget(self.progress_bar)
|
|
|
|
# 默认禁用工具栏按钮
|
|
self.refresh_button.setEnabled(False)
|
|
self.scan_button.setEnabled(False)
|
|
|
|
def switch_partition(self):
|
|
"""切换分区"""
|
|
dialog = PartitionDialog(self.disk_manager, self)
|
|
|
|
if dialog.exec_() == PartitionDialog.Accepted:
|
|
partition = dialog.get_selected_partition()
|
|
|
|
if partition:
|
|
self.load_partition(partition)
|
|
|
|
def load_partition(self, partition: Partition):
|
|
"""加载分区"""
|
|
self.current_partition = partition
|
|
self.status_bar.showMessage(f"正在挂载分区 {partition.device}...")
|
|
|
|
# 挂载分区
|
|
success, result = self.disk_manager.mount_partition(partition)
|
|
|
|
if success:
|
|
self.current_mount_point = result
|
|
|
|
# 更新界面
|
|
self.partition_label.setText(
|
|
f"当前分区:{partition.device} "
|
|
f"({partition.mount_point or '未挂载'}) "
|
|
f"- {partition.used_formatted} / {partition.size_formatted} "
|
|
f"({partition.use_percent_formatted})"
|
|
)
|
|
|
|
# 启用工具栏按钮
|
|
self.refresh_button.setEnabled(True)
|
|
self.scan_button.setEnabled(True)
|
|
|
|
# 加载根目录
|
|
self.file_browser.load_directory(self.current_mount_point)
|
|
|
|
# 检测系统日志
|
|
self.check_system_logs()
|
|
|
|
self.status_bar.showMessage(f"已加载分区 {partition.device}")
|
|
else:
|
|
QMessageBox.critical(
|
|
self,
|
|
"错误",
|
|
f"挂载分区失败:\n{result}"
|
|
)
|
|
self.status_bar.showMessage("挂载分区失败")
|
|
|
|
def check_system_logs(self):
|
|
"""检测系统日志大小"""
|
|
if not self.current_mount_point:
|
|
return
|
|
|
|
logs, total_size = self.file_scanner.get_system_logs(self.current_mount_point)
|
|
|
|
# 如果大于 1GB,显示快捷删除按钮
|
|
if total_size > 1024 * 1024 * 1024:
|
|
self.delete_logs_button.setText(
|
|
f"🗑️ 快捷删除系统日志 ({format_size(total_size)})"
|
|
)
|
|
self.delete_logs_button.show()
|
|
self._cached_logs = logs
|
|
self._cached_logs_size = total_size
|
|
else:
|
|
self.delete_logs_button.hide()
|
|
|
|
def quick_delete_logs(self):
|
|
"""快捷删除系统日志"""
|
|
if not hasattr(self, '_cached_logs') or not self._cached_logs:
|
|
return
|
|
|
|
dialog = LogDeleteConfirmDialog(
|
|
self._cached_logs,
|
|
self._cached_logs_size,
|
|
self
|
|
)
|
|
|
|
if dialog.exec_() == LogDeleteConfirmDialog.Accepted and dialog.is_confirmed():
|
|
# 删除日志文件
|
|
deleted_size = 0
|
|
errors = []
|
|
|
|
for log_file in self._cached_logs:
|
|
success, message = safe_delete(
|
|
log_file.path,
|
|
use_sudo=True,
|
|
sudo_password=self.sudo_password
|
|
)
|
|
|
|
if success:
|
|
deleted_size += log_file.size
|
|
else:
|
|
errors.append(f"{log_file.name}: {message}")
|
|
|
|
# 显示结果
|
|
if errors:
|
|
QMessageBox.warning(
|
|
self,
|
|
"清理完成(有错误)",
|
|
f"成功清理 {len(self._cached_logs) - len(errors)} 个日志文件,\n"
|
|
f"释放 {format_size(deleted_size)} 空间。\n\n"
|
|
f"以下文件清理失败:\n" + "\n".join(errors[:10])
|
|
)
|
|
else:
|
|
QMessageBox.information(
|
|
self,
|
|
"清理成功",
|
|
f"成功清理 {len(self._cached_logs)} 个日志文件,\n"
|
|
f"释放 {format_size(deleted_size)} 空间。"
|
|
)
|
|
|
|
# 刷新
|
|
self.check_system_logs()
|
|
self.refresh()
|
|
|
|
def scan_directory_sizes(self):
|
|
"""统计目录大小"""
|
|
if not self.current_mount_point:
|
|
return
|
|
|
|
self.scan_button.setEnabled(False)
|
|
self.status_bar.showMessage("正在统计目录大小...")
|
|
|
|
# 在后台线程统计
|
|
self.scan_thread = DirectorySizeThread(
|
|
self.file_scanner,
|
|
self.current_mount_point
|
|
)
|
|
self.scan_thread.progress.connect(self.on_scan_progress)
|
|
self.scan_thread.finished.connect(self.on_scan_finished)
|
|
self.scan_thread.error.connect(self.on_scan_error)
|
|
self.scan_thread.start()
|
|
|
|
def on_scan_progress(self, progress: ScanProgress):
|
|
"""扫描进度更新"""
|
|
self.progress_bar.setText(
|
|
f"已扫描 {progress.scanned_dirs} 个目录, "
|
|
f"{progress.scanned_files} 个文件"
|
|
)
|
|
|
|
def on_scan_finished(self, sizes: dict):
|
|
"""扫描完成"""
|
|
self.dir_sizes = sizes
|
|
self.scan_button.setEnabled(True)
|
|
self.progress_bar.clear()
|
|
|
|
# 按大小排序
|
|
sorted_sizes = sorted(
|
|
sizes.items(),
|
|
key=lambda x: x[1],
|
|
reverse=True
|
|
)
|
|
|
|
# 显示结果
|
|
total_size = sum(sizes.values())
|
|
self.status_bar.showMessage(
|
|
f"统计完成,共 {len(sizes)} 个目录,"
|
|
f"总大小 {format_size(total_size)}"
|
|
)
|
|
|
|
# 可以在这里显示一个统计结果窗口
|
|
self.show_scan_results(sorted_sizes)
|
|
|
|
def on_scan_error(self, error_message: str):
|
|
"""扫描出错"""
|
|
self.scan_button.setEnabled(True)
|
|
self.progress_bar.clear()
|
|
self.status_bar.showMessage(f"统计失败:{error_message}")
|
|
|
|
QMessageBox.critical(
|
|
self,
|
|
"错误",
|
|
f"统计目录大小失败:\n{error_message}"
|
|
)
|
|
|
|
def show_scan_results(self, sorted_sizes: list):
|
|
"""显示扫描结果"""
|
|
# TODO: 可以创建一个专门的结果窗口
|
|
# 目前先在状态栏显示前几个大目录
|
|
if sorted_sizes:
|
|
top_dirs = sorted_sizes[:5]
|
|
info = "最大的目录: " + ", ".join(
|
|
f"{os.path.basename(p)} ({format_size(s)})"
|
|
for p, s in top_dirs
|
|
)
|
|
self.status_bar.showMessage(info)
|
|
|
|
def on_directory_changed(self, path: str):
|
|
"""目录改变"""
|
|
# 可以在这里更新界面状态
|
|
pass
|
|
|
|
def on_files_deleted(self, freed_size: int):
|
|
"""文件被删除"""
|
|
# 更新分区使用情况
|
|
if self.current_partition:
|
|
self.current_partition.used -= freed_size
|
|
self.current_partition.available += freed_size
|
|
|
|
self.partition_label.setText(
|
|
f"当前分区:{self.current_partition.device} "
|
|
f"({self.current_partition.mount_point or '未挂载'}) "
|
|
f"- {self.current_partition.used_formatted} / "
|
|
f"{self.current_partition.size_formatted} "
|
|
f"({self.current_partition.use_percent_formatted})"
|
|
)
|
|
|
|
def refresh(self):
|
|
"""刷新"""
|
|
self.file_browser.refresh()
|
|
|
|
def closeEvent(self, event: QCloseEvent):
|
|
"""关闭事件"""
|
|
# 卸载所有分区
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"确认退出",
|
|
"确定要退出吗?\n已挂载的分区将被卸载。",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.Yes
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.disk_manager.unmount_all()
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|