diff --git a/scripts/filters.py b/scripts/filters.py index c48f6f6..69c6b1d 100644 --- a/scripts/filters.py +++ b/scripts/filters.py @@ -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]: """Применяет фильтры в списку путей""" @@ -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 diff --git a/scripts/gui.py b/scripts/gui.py new file mode 100644 index 0000000..6d1bdce --- /dev/null +++ b/scripts/gui.py @@ -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: + def __init__(self, root: tk.Tk) -> None: + 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() diff --git a/scripts/main.py b/scripts/main.py index ecb9dc8..abbb5e7 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -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 @@ -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) diff --git a/scripts/models.py b/scripts/models.py index 2847740..6c9e3a2 100644 --- a/scripts/models.py +++ b/scripts/models.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from enum import StrEnum from pathlib import Path @@ -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", "Изменен"), +] diff --git a/scripts/printers.py b/scripts/printers.py index 5bf484a..3a8397e 100644 --- a/scripts/printers.py +++ b/scripts/printers.py @@ -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() diff --git a/scripts/scanner.py b/scripts/scanner.py index a20fbd5..8c5f5ee 100644 --- a/scripts/scanner.py +++ b/scripts/scanner.py @@ -1,5 +1,6 @@ from collections import defaultdict from pathlib import Path +from typing import Callable from models import PathInfo from tqdm import tqdm @@ -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]: """ Рекурсивно сканирует файловую систему, начиная с указанной корневой директории. @@ -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: @@ -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 diff --git a/scripts/sorters.py b/scripts/sorters.py index cb4c2aa..89ec0d5 100644 --- a/scripts/sorters.py +++ b/scripts/sorters.py @@ -1,24 +1,24 @@ from collections import defaultdict from pathlib import Path -from models import PathInfo +from models import PathInfo, SortBy -def sort_paths(paths_info: defaultdict[Path, PathInfo], paths: list[Path], sort_by: str, reverse: bool) -> None: +def sort_paths(paths_info: defaultdict[Path, PathInfo], paths: list[Path], sort_by: SortBy, reverse: bool) -> None: """Сортирует спискок путей по одному из следующих критериев: размер, количество файлов, имя, глубина вложенности, время создания, время изменения""" match sort_by: - case "size": - paths.sort(key=lambda path: paths_info[path].size, reverse=True ^ reverse) - case "files-count": - paths.sort(key=lambda path: paths_info[path].files_count, reverse=True ^ reverse) - case "name": - paths.sort(key=lambda path: path.name, reverse=False ^ reverse) - case "depth": - paths.sort(key=lambda path: paths_info[path].depth, reverse=False ^ reverse) - case "creation-date": - paths.sort(key=lambda path: paths_info[path].birthtime, reverse=True ^ reverse) - case "change-date": - paths.sort(key=lambda path: paths_info[path].last_change_time, reverse=True ^ reverse) + case SortBy.SIZE: + paths.sort(key=lambda path: paths_info[path].size, reverse=not reverse) + case SortBy.FILES_COUNT: + paths.sort(key=lambda path: paths_info[path].files_count, reverse=not reverse) + case SortBy.NAME: + paths.sort(key=lambda path: path.name, reverse=reverse) + case SortBy.DEPTH: + paths.sort(key=lambda path: paths_info[path].depth, reverse=reverse) + case SortBy.CREATION_DATE: + paths.sort(key=lambda path: paths_info[path].birthtime, reverse=not reverse) + case SortBy.CHANGE_DATE: + paths.sort(key=lambda path: paths_info[path].last_change_time, reverse=not reverse) case _: - raise ValueError(sort_by, "Unknown sort option") + raise ValueError("Unknown sort option") diff --git a/scripts/test_sort_paths.py b/scripts/test_sort_paths.py index 77dad4c..7ebcb58 100644 --- a/scripts/test_sort_paths.py +++ b/scripts/test_sort_paths.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from models import PathInfo +from models import PathInfo, SortBy from sorters import sort_paths @@ -40,14 +40,14 @@ def sample_data() -> defaultdict[Path, PathInfo]: def test_sort_by_size_asc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "size", reverse=True) + sort_paths(sample_data, paths, SortBy("size"), reverse=True) sizes = [sample_data[p].size for p in paths] assert sizes == [50, 100, 200, 300] def test_sort_by_size_desc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "size", reverse=False) + sort_paths(sample_data, paths, SortBy("size"), reverse=False) sizes = [sample_data[p].size for p in paths] assert sizes == [300, 200, 100, 50] @@ -61,7 +61,7 @@ def test_sort_by_files_count_asc(sample_data: defaultdict[Path, PathInfo]) -> No data[d].files_count = 0 paths = list(data.keys()) - sort_paths(data, paths, "files-count", reverse=True) + sort_paths(data, paths, SortBy("files-count"), reverse=True) counts = [data[p].files_count for p in paths] assert counts == [0, 3, 7, 10] @@ -75,80 +75,74 @@ def test_sort_by_files_count_desc(sample_data: defaultdict[Path, PathInfo]) -> N data[d].files_count = 0 paths = list(data.keys()) - sort_paths(data, paths, "files-count", reverse=False) + sort_paths(data, paths, SortBy("files-count"), reverse=False) counts = [data[p].files_count for p in paths] assert counts == [10, 7, 3, 0] def test_sort_by_name_asc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "name", reverse=False) + sort_paths(sample_data, paths, SortBy("name"), reverse=False) names = [p.name for p in paths] assert names == ["a.txt", "b.txt", "c.txt", "very_important.pdf"] def test_sort_by_name_desc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "name", reverse=True) + sort_paths(sample_data, paths, SortBy("name"), reverse=True) names = [p.name for p in paths] assert names == ["very_important.pdf", "c.txt", "b.txt", "a.txt"] def test_sort_by_depth_asc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "depth", reverse=False) + sort_paths(sample_data, paths, SortBy("depth"), reverse=False) depths = [sample_data[p].depth for p in paths] assert depths == [1, 1, 1, 2] def test_sort_by_depth_desc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "depth", reverse=True) + sort_paths(sample_data, paths, SortBy("depth"), reverse=True) depths = [sample_data[p].depth for p in paths] assert depths == [2, 1, 1, 1] def test_sort_by_creation_date_asc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "creation-date", reverse=True) + sort_paths(sample_data, paths, SortBy("creation-date"), reverse=True) times = [sample_data[p].birthtime for p in paths] assert times == [500.0, 1000.0, 2500.0, 3000.0] def test_sort_by_creation_date_desc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "creation-date", reverse=False) + sort_paths(sample_data, paths, SortBy("creation-date"), reverse=False) times = [sample_data[p].birthtime for p in paths] assert times == [3000.0, 2500.0, 1000.0, 500.0] def test_sort_by_change_date_asc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "change-date", reverse=True) + sort_paths(sample_data, paths, SortBy("change-date"), reverse=True) times = [sample_data[p].last_change_time for p in paths] assert times == [1500.0, 2000.0, 2500.0, 3000.0] def test_sort_by_change_date_desc(sample_data: defaultdict[Path, PathInfo]) -> None: paths = list(sample_data.keys()) - sort_paths(sample_data, paths, "change-date", reverse=False) + sort_paths(sample_data, paths, SortBy("change-date"), reverse=False) times = [sample_data[p].last_change_time for p in paths] assert times == [3000.0, 2500.0, 2000.0, 1500.0] def test_sort_empty_list(sample_data: defaultdict[Path, PathInfo]) -> None: paths: list[Path] = [] - sort_paths(sample_data, paths, "size", False) + sort_paths(sample_data, paths, SortBy("size"), False) assert paths == [] def test_sort_single_element(sample_data: defaultdict[Path, PathInfo]) -> None: single_path = [list(sample_data.keys())[0]] - sort_paths(sample_data, single_path, "size", True) + sort_paths(sample_data, single_path, SortBy("size"), True) assert len(single_path) == 1 - - -def test_unknown_sort_criterion_raises_error(sample_data: defaultdict[Path, PathInfo]) -> None: - paths = list(sample_data.keys()) - with pytest.raises(ValueError, match="Unknown sort option"): - sort_paths(sample_data, paths, "invalid", False) diff --git a/scripts/ui_builder.py b/scripts/ui_builder.py new file mode 100644 index 0000000..8584932 --- /dev/null +++ b/scripts/ui_builder.py @@ -0,0 +1,59 @@ +import tkinter as tk +from tkinter import ttk + +from models import COLUMNS + + +class UIBuilder: + @staticmethod + def create_directory_tree(parent: tk.PanedWindow) -> ttk.Treeview: + """Создаёт и настраивает Treeview для дерева каталогов.""" + tree_frame = tk.Frame(parent) + + tree = ttk.Treeview(tree_frame, columns=(COLUMNS[1].id, COLUMNS[2].id, COLUMNS[3].id), show="tree headings") + + for i in range(4): + tree.heading(COLUMNS[i].id, text=COLUMNS[i].text, anchor=tk.W) + + tree.column(COLUMNS[1].id, width=120, anchor=tk.W) + tree.column(COLUMNS[2].id, width=100) + tree.column(COLUMNS[3].id, width=100) + + vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview) + hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview) + tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) + + tree.grid(row=0, column=0, sticky="nsew") + vsb.grid(row=0, column=1, sticky="ns") + hsb.grid(row=1, column=0, sticky="ew") + + tree_frame.grid_rowconfigure(0, weight=1) + tree_frame.grid_columnconfigure(0, weight=1) + + parent.add(tree_frame) + + return tree + + @staticmethod + def create_extension_tree(parent: tk.PanedWindow) -> ttk.Treeview: + """Создаёт и настраивает Treeview для статистики по расширениям.""" + ext_frame = tk.Frame(parent) + + ext_tree = ttk.Treeview(ext_frame, columns=("ext", "count", "size"), show="headings", height=15) + + ext_tree.heading("ext", text="Расширение", anchor=tk.W) + ext_tree.heading("count", text="Файлы", anchor=tk.W) + ext_tree.heading("size", text="Размер", anchor=tk.W) + + ext_tree.column("ext", width=100, anchor=tk.W) + ext_tree.column("count", width=70, anchor=tk.W) + ext_tree.column("size", width=100, anchor=tk.W) + + ext_vsb = ttk.Scrollbar(ext_frame, orient="vertical", command=ext_tree.yview) + ext_tree.configure(yscrollcommand=ext_vsb.set) + + ext_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + ext_vsb.pack(side=tk.RIGHT, fill=tk.Y) + + parent.add(ext_frame) + return ext_tree diff --git a/scripts/utils.py b/scripts/utils.py index abe82c4..9d2d73d 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,4 +1,5 @@ import typing +from datetime import datetime from pathlib import Path import win32security @@ -13,6 +14,11 @@ def convert_size(size_bytes: float) -> str: return f"{size_bytes:.1f}P" +def convert_time(timestamp: float) -> str: + dt = datetime.fromtimestamp(timestamp) + return dt.strftime("%d-%m-%Y %H:%M") + + def get_owner(path: Path) -> typing.Any: """Получает имя владельца файла""" path_str = str(path.resolve())