首次代码提交

This commit is contained in:
2026-06-26 16:54:55 +08:00
parent 76515ac14e
commit 9bbcf5a710
13 changed files with 3109 additions and 0 deletions
+373
View File
@@ -0,0 +1,373 @@
"""
主窗口
"""
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()