Первый выпуск SysDevCleaner
Анализ 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>
This commit is contained in:
172
sysdevcleaner/core/model.py
Normal file
172
sysdevcleaner/core/model.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Базовые структуры данных 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
|
||||
Reference in New Issue
Block a user