Анализ Windows и очистка кешей, мусора и остатков разработки. Ядро: - каталог правил: 228 правил путей (496 шаблонов) + 43 правила артефактов проектов — Windows, браузеры, кеши сборки, IDE, приложения, пакетные менеджеры; - рекурсивный поиск node_modules, target, build, __pycache__, .venv и т.п. с проверкой маркеров проекта, чтобы не ловить ложные срабатывания; - поиск крупных файлов и дубликатов (хеш по началу/середине/концу и полная сверка), инвентаризация установленных программ через реестр; - ограничение области сканирования по дискам; - рекомендации и текстовый отчёт. Безопасность: - четыре уровня риска, автоматически отмечается только «Безопасно»; - список защищённых путей проверяется и при выборе, и перед удалением; - для каталогов вроде %TEMP% удаляется содержимое, а не сам каталог; - удаление в Корзину по умолчанию, пробный запуск, журнал очисток. Интерфейс на Dear PyGui: - JetBrains Mono Nerd Font: кириллица и иконки в одном шрифте; - тёмная и светлая темы, адаптивная раскладка; - заголовок окна ставится через SetWindowTextW — Dear PyGui отдаёт его в ANSI-функцию как UTF-8 и портит кириллицу; - запуск всегда с правами администратора: манифест UAC в exe и самоперезапуск через ShellExecuteW при работе из исходников. Сборка в один exe через PyInstaller со вшитым шрифтом и значком. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
"""Базовые структуры данных SysDevCleaner."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from enum import IntEnum
|
|
from typing import Iterable
|
|
|
|
|
|
class Risk(IntEnum):
|
|
"""Насколько опасно удалять находку."""
|
|
|
|
SAFE = 0 # мусор в чистом виде, пересоздаётся сам
|
|
LOW = 1 # кеш: удаление вызовет лишь замедление первого запуска
|
|
MEDIUM = 2 # потеря состояния/настроек/сессий, нужна пересборка
|
|
HIGH = 3 # можно сломать проект или систему — только вручную
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return {
|
|
Risk.SAFE: "Безопасно",
|
|
Risk.LOW: "Низкий риск",
|
|
Risk.MEDIUM: "Средний риск",
|
|
Risk.HIGH: "Высокий риск",
|
|
}[self]
|
|
|
|
@property
|
|
def color(self) -> str:
|
|
return {
|
|
Risk.SAFE: "#2e9e5b",
|
|
Risk.LOW: "#4a90d9",
|
|
Risk.MEDIUM: "#d99a2b",
|
|
Risk.HIGH: "#d9534f",
|
|
}[self]
|
|
|
|
|
|
class Category(IntEnum):
|
|
WINDOWS = 0
|
|
BROWSERS = 1
|
|
DEV_CACHE = 2
|
|
DEV_PROJECT = 3
|
|
APPS = 4
|
|
LOGS = 5
|
|
PACKAGE_MANAGERS = 6
|
|
BIG_FILES = 7
|
|
DUPLICATES = 8
|
|
PROGRAMS = 9
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return {
|
|
Category.WINDOWS: "Система Windows",
|
|
Category.BROWSERS: "Браузеры",
|
|
Category.DEV_CACHE: "Кеши разработки",
|
|
Category.DEV_PROJECT: "Артефакты проектов",
|
|
Category.APPS: "Приложения",
|
|
Category.LOGS: "Логи и отчёты",
|
|
Category.PACKAGE_MANAGERS: "Пакетные менеджеры",
|
|
Category.BIG_FILES: "Крупные файлы",
|
|
Category.DUPLICATES: "Дубликаты",
|
|
Category.PROGRAMS: "Установленные программы",
|
|
}[self]
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
return {
|
|
Category.WINDOWS: "🪟",
|
|
Category.BROWSERS: "🌐",
|
|
Category.DEV_CACHE: "⚙",
|
|
Category.DEV_PROJECT: "📦",
|
|
Category.APPS: "🎮",
|
|
Category.LOGS: "📄",
|
|
Category.PACKAGE_MANAGERS: "🧰",
|
|
Category.BIG_FILES: "🐘",
|
|
Category.DUPLICATES: "👯",
|
|
Category.PROGRAMS: "🗂",
|
|
}[self]
|
|
|
|
|
|
@dataclass
|
|
class Finding:
|
|
"""Одна находка: путь (или группа путей), который можно удалить."""
|
|
|
|
path: str
|
|
title: str # человекочитаемое имя
|
|
category: Category
|
|
risk: Risk
|
|
size: int = 0 # байты
|
|
files: int = 0
|
|
dirs: int = 0
|
|
mtime: float = 0.0 # последнее изменение (unix)
|
|
detail: str = "" # подсказка что это и чем грозит удаление
|
|
group: str = "" # имя правила/группы для сводки
|
|
is_dir: bool = True
|
|
delete_contents_only: bool = False # чистим содержимое, сам каталог оставляем
|
|
members: list[str] = field(default_factory=list) # для дубликатов/мульти-путей
|
|
selected: bool = False
|
|
extra: dict = field(default_factory=dict)
|
|
|
|
@property
|
|
def age_days(self) -> float:
|
|
if not self.mtime:
|
|
return 0.0
|
|
return max(0.0, (time.time() - self.mtime) / 86400.0)
|
|
|
|
@property
|
|
def key(self) -> str:
|
|
return os.path.normcase(os.path.abspath(self.path))
|
|
|
|
|
|
@dataclass
|
|
class Program:
|
|
"""Установленная программа из реестра."""
|
|
|
|
name: str
|
|
publisher: str = ""
|
|
version: str = ""
|
|
install_date: str = ""
|
|
install_location: str = ""
|
|
uninstall_cmd: str = ""
|
|
registry_size: int = 0 # EstimatedSize из реестра
|
|
disk_size: int = 0 # реально посчитанный размер
|
|
last_used: float = 0.0 # mtime самого свежего файла
|
|
source: str = "" # HKLM / HKCU / WOW64
|
|
|
|
@property
|
|
def best_size(self) -> int:
|
|
return self.disk_size or self.registry_size
|
|
|
|
@property
|
|
def idle_days(self) -> float:
|
|
if not self.last_used:
|
|
return -1.0
|
|
return max(0.0, (time.time() - self.last_used) / 86400.0)
|
|
|
|
|
|
@dataclass
|
|
class Recommendation:
|
|
title: str
|
|
body: str
|
|
gain: int = 0 # сколько освободится, байт
|
|
severity: int = 1 # 0 инфо, 1 совет, 2 важно
|
|
findings: list[Finding] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class ScanResult:
|
|
findings: list[Finding] = field(default_factory=list)
|
|
programs: list[Program] = field(default_factory=list)
|
|
recommendations: list[Recommendation] = field(default_factory=list)
|
|
disks: list[dict] = field(default_factory=list)
|
|
errors: list[str] = field(default_factory=list)
|
|
started: float = 0.0
|
|
finished: float = 0.0
|
|
scanned_paths: int = 0
|
|
|
|
@property
|
|
def duration(self) -> float:
|
|
return max(0.0, (self.finished or time.time()) - self.started)
|
|
|
|
def total(self, cats: Iterable[Category] | None = None) -> int:
|
|
cs = set(cats) if cats else None
|
|
return sum(f.size for f in self.findings if cs is None or f.category in cs)
|
|
|
|
def by_category(self) -> dict[Category, list[Finding]]:
|
|
out: dict[Category, list[Finding]] = {}
|
|
for f in self.findings:
|
|
out.setdefault(f.category, []).append(f)
|
|
for lst in out.values():
|
|
lst.sort(key=lambda x: -x.size)
|
|
return out
|