Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions scripts/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def apply_filters(
paths_info: defaultdict[Path, PathInfo],
extension: str | None,
limit: int | None,
dirs_only: bool | None,
dirs_only: bool,
max_depth: int | None,
) -> list[Path]:
"""Применяет фильтры в списку путей"""
Expand All @@ -21,16 +21,16 @@ def apply_filters(
if dirs_only and not path.is_dir():
continue

if max_depth is not None and paths_info[path].depth > max_depth:
if max_depth and paths_info[path].depth > max_depth:
continue

if extension is not None:
if path.suffix != ('.' + extension):
if extension:
if path.suffix != f'.{extension}':
continue
filtered_paths.append(path)
pbar.close()

if limit is not None:
if limit:
filtered_paths = filtered_paths[:limit]

return filtered_paths
206 changes: 206 additions & 0 deletions scripts/gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import threading
import tkinter as tk
from collections import defaultdict
from functools import partial
from pathlib import Path
from tkinter import filedialog, messagebox

from grouper import group_paths
from models import PathInfo
from scanner import scan_all_directories
from ui_builder import COLUMNS, UIBuilder
from utils import convert_size, convert_time


class DiskUsageGUI:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а мб немного декомпозировать класс?

def __init__(self, root: tk.Tk) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

почему все публичное?

self._root = root
self._root.title("Disk Usage Analyzer")
self._root.geometry("1000x700")

self._init_state()

self._btn_select = tk.Button(root, text="Выбрать папку", command=self._select_folder)
self._btn_select.pack(pady=10)

self._status_var = tk.StringVar()
self._status_label = tk.Label(root, textvariable=self._status_var, fg="gray")
self._status_label.pack()

main_pane = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=6)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

self._tree = UIBuilder.create_directory_tree(main_pane)

self._ext_tree = UIBuilder.create_extension_tree(main_pane)

self._setup_tree_sorting()

root.after(100, lambda: main_pane.sash_place(0, 700, 0))

def _init_state(self) -> None:
"""Инициализация внутренних данных приложения."""
self._paths_info: defaultdict[Path, PathInfo] = defaultdict()
self._paths_ids: defaultdict[Path, str] = defaultdict()
self._graph: defaultdict[Path, list[PathInfo]] = defaultdict(list)
self._folder: str = ""
self._current_root: Path | None = None
self._sorter_parameter: tuple[str, bool] = (COLUMNS[1].id, True)

def _setup_tree_sorting(self) -> None:
"""Подключает обработчики сортировки к заголовкам дерева."""
for column in COLUMNS:
self._tree.heading(column.id, command=partial(self._on_header_click, column.id))

def _update_heading_texts(self) -> None:
"""Обновляет текст заголовка (добавляет стрелочки)"""
for column in COLUMNS:
if column.id == self._sorter_parameter[0]:
arrow = " ▼" if self._sorter_parameter[1] else " ▲"
self._tree.heading(column.id, text=column.text + arrow)
else:
self._tree.heading(column.id, text=column.text)

def _on_header_click(self, parameter: str) -> None:
"""Обрабатывает события нажатия на заголовок"""
if not self._graph:
return
self._tree.delete(*self._tree.get_children())
self._paths_ids.clear()
if parameter == self._sorter_parameter[0]:
self._sorter_parameter = (self._sorter_parameter[0], self._sorter_parameter[1] ^ True)
else:
self._sorter_parameter = (parameter, False)

self._sort_graph()
self._root.after(0, self._show_results, Path(self._folder).resolve(), list(self._paths_info.values()))

def _select_folder(self) -> None:
"""Выбирает папку для сканирования"""
self._folder = filedialog.askdirectory(title="Выберите папку")
if self._folder:
self._start_analysis(self._folder)

def _progress_callback(self, count: int) -> None:
"""Прогресс-бар операции сканирования"""
self._root.after(0, lambda: self._status_var.set(f"Сканирование: {count} элементов"))

def _start_analysis(self, folder: str) -> None:
"""Запускает сканирование выбранной директории"""
self._status_var.set("Сканирование: 0 элементов")
self._btn_select.config(state="disabled")
self._tree.delete(*self._tree.get_children())
self._ext_tree.delete(*self._ext_tree.get_children())

self._paths_ids.clear()

thread = threading.Thread(target=self._analyze, args=(folder,), daemon=True)
thread.start()

def _analyze(self, folder: str) -> None:
"""Анализирует выбранную директорию"""
try:
root_path = Path(folder).resolve()
if not root_path.exists():
raise FileNotFoundError(f"Путь не существует: {root_path}")

self._paths_info = scan_all_directories(root_path, self._progress_callback)

self._graph = self._create_graph(root_path, list(self._paths_info.values()))

self.ext_stats = sorted(
group_paths(self._paths_info, "extension").items(), key=lambda x: x[1][COLUMNS[1].id], reverse=True
)

self._sorter_parameter = (COLUMNS[1].id, True)
self._sort_graph()
self._root.after(0, self._show_results, root_path, list(self._paths_info.values()))

except Exception as e:
self._root.after(0, self._show_error, str(e))

def _show_error(self, msg: str) -> None:
"""Выводит сообщение об ошибке"""
messagebox.showerror("Ошибка", msg)
self._status_var.set("Ошибка")
self._btn_select.config(state="normal")

def _sort_graph(self) -> None:
"""Сортирует граф"""
for node in self._graph:
match self._sorter_parameter[0]:
case "#0":
self._graph[node].sort(key=lambda x: x.path.name, reverse=self._sorter_parameter[1])
case "size":
self._graph[node].sort(key=lambda x: x.size, reverse=self._sorter_parameter[1])
case "files_count":
self._graph[node].sort(key=lambda x: x.files_count, reverse=self._sorter_parameter[1])
case "last_change_time":
self._graph[node].sort(key=lambda x: x.last_change_time, reverse=self._sorter_parameter[1])
self._update_heading_texts()

def _insert_items(self, parent_path: Path, parent_tree_id: str) -> None:
"""Добавляет новую директорию в дерево"""
children = self._graph.get(parent_path, [])

for child in children:
name = child.path.name
size_str = convert_size(child.size)
files_count = child.files_count
last_change_time = convert_time(child.last_change_time)

child_id = self._tree.insert(
parent_tree_id, "end", text=name, values=(size_str, files_count, last_change_time)
)

self._paths_ids[child.path] = child_id
if child.is_dir:
self._insert_items(child.path, child_id)

def _update_extension_stats(self) -> None:
"""Обновляет таблицу расширений."""
if not self._paths_info:
return

for ext, data in self.ext_stats:
self._ext_tree.insert("", "end", values=(ext, data["count"], convert_size(data[COLUMNS[1].id])))

def _show_results(self, root_path: Path, display_items: list[PathInfo]) -> None:
"""Выводит на экран все дерево директорий."""
self._current_root = root_path
self._status_var.set(f"Просканировано: {len(display_items)} элементов")
self._btn_select.config(state="normal")

self._paths_ids.clear()
root_name = root_path.name
root_item = self._tree.insert(
"",
"end",
text=root_name,
values=(
convert_size(self._paths_info[root_path].size),
self._paths_info[root_path].files_count,
convert_time(self._paths_info[root_path].last_change_time),
),
)
self._paths_ids[root_path] = root_item

self._insert_items(root_path, root_item)
self._tree.item(root_item, open=True)
self._update_extension_stats()

def _create_graph(self, root_path: Path, display_items: list[PathInfo]) -> defaultdict[Path, list[PathInfo]]:
"""Создает граф вложенности всех директорий."""
graph = defaultdict(list)
for item in display_items:
if item.path == root_path:
continue
parent = item.path.parent
graph[parent].append(item)
return graph


if __name__ == "__main__":
root = tk.Tk()
app = DiskUsageGUI(root)
root.mainloop()
3 changes: 2 additions & 1 deletion scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from cli import parse_arguments
from filters import apply_filters
from grouper import group_paths
from models import SortBy
from printers import print_groups, print_paths
from scanner import scan_all_directories
from sorters import sort_paths
Expand All @@ -24,7 +25,7 @@ def main() -> None:
print_groups(groups)
return
filtered_paths = apply_filters(paths_info, args.extension, args.limit, args.dirs_only, args.max_depth)
sort_paths(paths_info, filtered_paths, args.sort_by, args.reverse)
sort_paths(paths_info, filtered_paths, SortBy(args.sort_by), args.reverse)
print_paths(paths_info, root_path, filtered_paths)


Expand Down
24 changes: 24 additions & 0 deletions scripts/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path


Expand All @@ -25,3 +26,26 @@ class PathInfo:
last_change_time: float
is_dir: bool
files_count: int


class SortBy(StrEnum):
SIZE = "size"
FILES_COUNT = "files-count"
NAME = "name"
DEPTH = "depth"
CREATION_DATE = "creation-date"
CHANGE_DATE = "change-date"


class Column:
def __init__(self, id: str, text: str):
self.id = id
self.text = text


COLUMNS = [
Column("#0", "Имя"),
Column("size", "Размер"),
Column("files_count", "Файлы"),
Column("last_change_time", "Изменен"),
]
55 changes: 27 additions & 28 deletions scripts/printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,42 @@

def print_paths(paths_info: defaultdict[Path, PathInfo], root_path: Path, paths: list[Path]) -> None:
"""Выводит все окончательные пути до файлов и директорий"""
file = open('result.txt', 'w', encoding='utf-8')
with open('result.txt', 'w', encoding='utf-8') as file:
print(f"{'Размер':<{SIZE_WIDTH}}" f"{'Файлы':<{FILES_COUNT_WIDTH}}" f"Путь", file=file)
print("-" * (SIZE_WIDTH + FILES_COUNT_WIDTH + PATH_WIDTH), file=file)

print(f"{'Размер':<{SIZE_WIDTH}}" f"{'Файлы':<{FILES_COUNT_WIDTH}}" f"Путь", file=file)
print("-" * (SIZE_WIDTH + FILES_COUNT_WIDTH + PATH_WIDTH), file=file)
pbar = tqdm(total=len(paths), desc="Вывод результатов", unit=" элементов")

pbar = tqdm(total=len(paths), desc="Вывод результатов", unit=" элементов")
for path in paths:
info = paths_info[path]
size_str = convert_size(info.size)
files_str = str(info.files_count)
path_str = str(path.relative_to(root_path)) + ("\\" if path.is_dir() else "")

for path in paths:
info = paths_info[path]
size_str = convert_size(info.size)
files_str = str(info.files_count)
path_str = str(path.relative_to(root_path)) + ("\\" if path.is_dir() else "")
if len(path_str) > PATH_WIDTH:
path_str = '~' + path_str[-(PATH_WIDTH - 1) :]

if len(path_str) > PATH_WIDTH:
path_str = '~' + path_str[-(PATH_WIDTH - 1) :]

print(f"{size_str:<{SIZE_WIDTH}}" f"{files_str:<{FILES_COUNT_WIDTH}}" f"{path_str}", file=file)
pbar.update()
pbar.close()
print(f"{size_str:<{SIZE_WIDTH}}" f"{files_str:<{FILES_COUNT_WIDTH}}" f"{path_str}", file=file)
pbar.update()
pbar.close()


def print_groups(groups: defaultdict[str, dict[str, int]]) -> None:
"""Выводит все получившиеся группы"""
file = open('result.txt', 'w', encoding='utf-8')
list_groups = list(groups.keys())
list_groups.sort(key=lambda group: groups[group]['size'], reverse=True)
with open('result.txt', 'w', encoding='utf-8') as file:
list_groups = list(groups.keys())
list_groups.sort(key=lambda group: groups[group]['size'], reverse=True)

groups_width = max(len(group) for group in list_groups) + 5
print(f"{'Группы':<{groups_width}}" f"{'Файлы':<{FILES_COUNT_WIDTH}}" f"{'Размер':<{SIZE_WIDTH}}", file=file)
print("-" * (groups_width + SIZE_WIDTH + FILES_COUNT_WIDTH), file=file)
groups_width = max(len(group) for group in list_groups) + 5
print(f"{'Группы':<{groups_width}}" f"{'Файлы':<{FILES_COUNT_WIDTH}}" f"{'Размер':<{SIZE_WIDTH}}", file=file)
print("-" * (groups_width + SIZE_WIDTH + FILES_COUNT_WIDTH), file=file)

pbar = tqdm(total=len(list_groups), desc="Вывод результатов", unit=" элементов")
pbar = tqdm(total=len(list_groups), desc="Вывод результатов", unit=" элементов")

for group in list_groups:
files_str = str(groups[group]['count'])
size_str = convert_size(groups[group]['size'])
for group in list_groups:
files_str = str(groups[group]['count'])
size_str = convert_size(groups[group]['size'])

print(f"{group:<{groups_width}}" f"{files_str:<{FILES_COUNT_WIDTH}}" f"{size_str:<{SIZE_WIDTH}}", file=file)
pbar.update()
pbar.close()
print(f"{group:<{groups_width}}" f"{files_str:<{FILES_COUNT_WIDTH}}" f"{size_str:<{SIZE_WIDTH}}", file=file)
pbar.update()
pbar.close()
7 changes: 6 additions & 1 deletion scripts/scanner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import defaultdict
from pathlib import Path
from typing import Callable

from models import PathInfo
from tqdm import tqdm
Expand All @@ -25,7 +26,7 @@ def update_directory_sizes(paths_info: defaultdict[Path, PathInfo], path: Path,


def scan_all_directories(
root_path: Path,
root_path: Path, progress_callback: Callable[[int], None] | None = None
) -> defaultdict[Path, PathInfo]:
"""
Рекурсивно сканирует файловую систему, начиная с указанной корневой директории.
Expand All @@ -38,6 +39,7 @@ def scan_all_directories(
print(f"Анализ директории: {root_path}")
paths_info: defaultdict[Path, PathInfo] = defaultdict()
pbar = tqdm(desc="Сканирование", unit=" элементов")
count = 0

paths_to_scan = [root_path]
while paths_to_scan:
Expand All @@ -64,5 +66,8 @@ def scan_all_directories(
)

pbar.update(1)
count += 1
if progress_callback is not None:
progress_callback(count)
pbar.close()
return paths_info
Loading