From f43153c4faa563ebf17200944199938247108acf Mon Sep 17 00:00:00 2001 From: AntynK <124431125+AntynK@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:00:46 +0300 Subject: [PATCH] v.1.0.0' --- CONTRIBUTING.md | 7 +++ CONTRIBUTORS.md | 0 LICENCE | 21 ++++++++ README.md | 55 +++++++++++++++++++- README_UA.md | 55 ++++++++++++++++++++ data/backup_manager.py | 87 ++++++++++++++++++++++---------- data/controls/backup_data_row.py | 14 +++-- data/controls/backup_entry.py | 15 +++--- data/controls/backups_editor.py | 32 +++++++++--- data/controls/backups_view.py | 50 +++++++++++++++--- data/controls/world_view.py | 1 + data/dialogs/change_backup.py | 2 +- data/dialogs/change_settings.py | 26 +++++----- data/path_utils.py | 15 ++++-- data/settings.py | 19 ++++--- data/utils.py | 11 ++-- 16 files changed, 325 insertions(+), 85 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS.md create mode 100644 LICENCE create mode 100644 README_UA.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..43b2a4f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +## Contributors +Please follow these steps in order to merge your work: + +**PEP8 is generally to be followed.** +1. Clone repo and create new branch. +2. Update README.md and CONTRIBUTORS.md, if necessary. +3. Open a Pull Request with a comprehensive description of changes. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..f58de2c --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Andrii Karandashov(AntynK) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2425122..8d21ed1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,57 @@ # Mc Backupper +Tool for making backups of Minecraft worlds from `saves` and `versions` folders. -Tool for making backups of Minecraft's world from `saves` and `versions` folders. +## Basic +### Creation +All data that you have entered during creation are stored separately from the backup file. +To change backup data, press and hold the left mouse button. +> [!NOTE] +> Clicking the backup name will open it in the file explorer. + +### Backup data +`File name` - the backup file name, by default is the world name. The creation date and `.zip` extension will be added after creation. +> [!NOTE] +> This cannot be changed after creation. + +`Title` - backup title, optional field. +`Pool ignore` - when checked, the backup is not included in the pool. + +### Backup pool +The pool (queue) automatically removes outdated backups. +By default, the pool is set to 4. This means if you have 4 backups (with the `Pool ignore` flag unchecked) and create a new backup, the oldest one will be deleted. +> [!NOTE] +> The program determines the oldest backup by the date that the user has entered. + +### Restoring and deleting +When restoring, the world folder will be permanently deleted and replaced with the folder from the backup. +> [!IMPORTANT] +> The program will immediately restore the world without popup windows. + +When deleting, the backup file will be removed. +> [!IMPORTANT] +> The program will immediately delete the backup without popup windows. + +### Backup structure +All backups from the `saves` folder are saved at `/saves//backups`. From the `versions` folder at `/versions//saves//backups`. + +`` - the folder where all backups are saved (can be changed in settings). +`` - version name. +`` - world name. +`backups` - folder where world backups are saved. + +> [!NOTE] +> Names are taken from folder names, so they may be different from what you see in the game. + +> [!TIP] +> To get the name of the world folder, in select world menu you can look below the world name, or you can open it when editing the world. + + +## Features +The program can automatically determine the backup creation date (if it is formatted like `YEAR-MONTH-DAY_HOUR-MINUTE-SECOND`), so you can transfer backups created with Minecraft. + +> [!NOTE] +> Backups that have been transferred in this way will be included in the pool. + +## Contributors +If you have ideas for improvement or want to contribute to the development of the project, please submit your contribution. See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/README_UA.md b/README_UA.md new file mode 100644 index 0000000..c11a4a6 --- /dev/null +++ b/README_UA.md @@ -0,0 +1,55 @@ +# Mc Backupper +Ця програма дає змогу робити резервні копії світів Minecraft з папок `saves` та `versions`. + +## Основне +### Створення +Всі налаштування, які ви вводите при створенні резервної копії, зберігаються окремо і не впливають на саму резервну копію. +Для того, щоб змінити дані про резервну копію, потрібно затиснути ЛКМ на ній. + +> [!NOTE] +> Натискання на назву резервної копії відкриє її в провіднику + +### Параметри резервної копії +`File name` - назва файлу резервної копії, за замовчуванням береться назва світу. Після створення до неї буде додано введений час та розширення `.zip`. +> [!NOTE] +> Його не можна змінювати після створення. + +`Title` - назва резервної копії, не обов'язкове поле. +`Pool ignore` - коли прапорець встановлений, резервна копія не входить до пулу. + +### Пул резервних копій +Пул (або черга) створений для того, щоб автоматично видаляти застарілі резервні копії. +За замовчуванням пул налаштовано на 4, тобто якщо при створенні нової резервної копії вже є 4 резервні копії (які не мають прапорця `Pool ignore`), то найстаріша буде видалена. +> [!NOTE] +> Програма визначає найстарішу копію за датою, яку ввів користувач. + +### Відновлення та видалення +При відновленні програма з початку видалить папку світу (її не можна буде відновити) та вставить папку з резервної копії. +> [!IMPORTANT] +> Програма одразу відновить резервну копію, без діалогових вікон. + +При видаленні програма видаляє файл резервної копії. +> [!IMPORTANT] +> Програма одразу видалить резервну копію, без діалогових вікон. +## Структура резервних копій +Всі резервні копії з папки `saves` зберігаються у папці `/saves//backups`, а з папки `versions` у `/versions//saves//backups`. + +`` - папка, де зберігаються всі резервні копії (її можна змінити в налаштуваннях). +`` - назва версії. +`` - назва світу. +`backups` - папка, де зберігаються резервні копії світу. + +> [!NOTE] +> Назви беруться з назв папок, тому вони можуть відрізнятися від того, як ви їх бачите в грі. + +> [!TIP] +> У меню вибору світу під назвою світу є назва папки, також під час редагування світу можна відкрити його папку. + +## Особливість +Програма може автоматично визначати дату створення резервної копії (якщо вона є в назві і має формат `РІК-МІСЯЦЬ-ДЕНЬ_ГОДИНА-ХВИЛИНА-СЕКУНДА`), тому ви можете перенести резервні копії, створені за допомогою Minecraft. +> [!NOTE] +> Копії, які були перенесені таким чином, входитимуть в пул. + + +## Контриб'ютори +Якщо у вас є ідеї щодо покращення чи бажання долучитися до розвитку проєкту, будь ласка, подайте свій внесок. Відкрийте [CONTRIBUTING.md](CONTRIBUTING.md) для детального ознайомлення. \ No newline at end of file diff --git a/data/backup_manager.py b/data/backup_manager.py index 4edcb6b..9a4befd 100644 --- a/data/backup_manager.py +++ b/data/backup_manager.py @@ -1,11 +1,13 @@ import json import shutil +import re from pathlib import Path from dataclasses import dataclass from datetime import datetime +from enum import Enum from data.path_utils import convert_world_path_to_backup, get_top_dir -from data.utils import convert_timestamp +from data.utils import convert_timestamp, FILE_DATETIME_FORMAT from data.settings import Settings BACKUPS_FOLDER = Path("backups") @@ -18,7 +20,7 @@ class Backup: name: str = "" title: str = "" created: int = 0 - pull_ignore: bool = True + pool_ingore: bool = True path: Path = Path() def __post_init__(self): @@ -32,6 +34,19 @@ def __eq__(self, other): return self.name == other +class SortKeys(Enum): + NAME = "name" + TITLE = "title" + CREATED = "created" + POOL_IGNORE = "pool_ingore" + + +def sort_backups( + backups: list[Backup], key: SortKeys, reverse: bool = False +) -> list[Backup]: + return sorted(backups, key=lambda e: getattr(e, key.value), reverse=reverse) + + class BackupManager: def __init__(self) -> None: self.backups: list[Backup] = [] @@ -39,18 +54,21 @@ def __init__(self) -> None: self.file_path = Path() self.world_path = Path() + def get_sorted_backups(self) -> list[Backup]: + return sort_backups(self.backups, SortKeys.CREATED) + def load(self, world_path: Path) -> None: self.backups.clear() self.world_path = world_path self.work_dir = convert_world_path_to_backup(world_path) self.file_path = self.work_dir.joinpath(BACKUPS_FILE) - if not self.file_path.is_file(): + if not self.work_dir.joinpath(BACKUPS_FOLDER).is_dir(): return try: data = json.loads(self.file_path.read_text("utf-8")) - except json.decoder.JSONDecodeError: + except (json.decoder.JSONDecodeError, OSError): data = {} if not isinstance(data, dict): @@ -64,14 +82,27 @@ def load(self, world_path: Path) -> None: def _check_backups_folder(self) -> None: for path in self.work_dir.joinpath(BACKUPS_FOLDER).iterdir(): - if path.name not in self.backups: - self.backups.append(Backup(name=path.name)) + file_name = path.name + if file_name not in self.backups: + self.backups.append( + Backup( + name=file_name, + created=self._get_timestamp_from_string(file_name), + pool_ingore=False, + ) + ) + + def _get_timestamp_from_string(self, string: str) -> int: + matched = re.match(r"\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}", string) + if matched is None: + return 0 + + return int(datetime.strptime(matched.group(), FILE_DATETIME_FORMAT).timestamp()) def save(self) -> None: self.work_dir.mkdir(exist_ok=True, parents=True) - result = {} - for backup in self.backups: - result.update(**self._save_backup(backup)) + result = dict(self._save_backup(backup) for backup in self.backups) + self.file_path.write_text( json.dumps(result, ensure_ascii=False, indent=4), encoding="utf-8" ) @@ -86,10 +117,10 @@ def delete(self, backup: Backup) -> None: def create(self, new_backup: Backup) -> None: if not self.world_path.is_dir(): return - filename = f"{new_backup.name} {convert_timestamp(new_backup.created)}" - + filename = f"{convert_timestamp(new_backup.created, FILE_DATETIME_FORMAT)}_{new_backup.name}" + new_backup.path = self.work_dir.joinpath(BACKUPS_FOLDER) new_backup.name = f"{filename}.{BACKUP_FILE_FORMAT}" - base_name = str(self.work_dir.joinpath(BACKUPS_FOLDER, filename)) + base_name = str(new_backup.path.joinpath(filename)) shutil.make_archive( base_name=base_name, @@ -100,7 +131,7 @@ def create(self, new_backup: Backup) -> None: self.backups.append(new_backup) self.save() - self._check_backup_pull() + self._check_backup_pool() def restore(self, backup: Backup) -> None: backup_file = self.work_dir.joinpath(BACKUPS_FOLDER, backup.name) @@ -111,18 +142,14 @@ def restore(self, backup: Backup) -> None: format=BACKUP_FILE_FORMAT, ) - def update(self, backup: Backup): - index = self.backups.index(backup) - self.backups[index] = backup - self.save() - - def _check_backup_pull(self) -> None: - pull = [backup for backup in self.backups if not backup.pull_ignore] - max_len = Settings().get_pull_size() - if len(pull) <= max_len: + def _check_backup_pool(self) -> None: + pool = [backup for backup in self.backups if not backup.pool_ingore] + max_len = Settings().get_pool_size() + if len(pool) <= max_len: return - pull.sort(key=lambda backup: backup.created, reverse=True) - for backup in pull[max_len:]: + + sorted_pool = sort_backups(pool, SortKeys.CREATED, True) + for backup in sorted_pool[max_len:]: self.delete(backup) def _load_backup(self, backup_data: dict, name: str) -> Backup: @@ -130,13 +157,17 @@ def _load_backup(self, backup_data: dict, name: str) -> Backup: result.name = name result.title = backup_data.get("title", "") result.created = backup_data.get("created", 0) - result.pull_ignore = backup_data.get("pull_ignore", True) + if "pull_ignore" in backup_data: # Compatibility with previous versions + backup_data["pool_ignore"] = backup_data["pull_ignore"] + + result.pool_ingore = backup_data.get("pool_ignore", True) + result.path = self.work_dir.joinpath(BACKUPS_FOLDER) return result - def _save_backup(self, backup: Backup) -> dict: + def _save_backup(self, backup: Backup) -> tuple[str, dict]: result = {} result["title"] = backup.title result["created"] = backup.created - result["pull_ignore"] = backup.pull_ignore - return {backup.name: result} + result["pool_ignore"] = backup.pool_ingore + return backup.name, result diff --git a/data/controls/backup_data_row.py b/data/controls/backup_data_row.py index 84e4c77..87e6531 100644 --- a/data/controls/backup_data_row.py +++ b/data/controls/backup_data_row.py @@ -2,15 +2,19 @@ import flet as ft from data.backup_manager import Backup -from data.utils import convert_timestamp, open_with_explorer +from data.utils import convert_timestamp, open_with_explorer, UI_DATETIME_FORMAT class BackupDataRow(ft.DataRow): def __init__( - self, backup: Backup, on_select_changed: Callable, on_long_press: Callable + self, + backup: Backup, + on_select_changed: Callable, + on_long_press: Callable, + index: int, ) -> None: super().__init__() - self.on_select_changed = lambda e: on_select_changed(backup) + self.on_select_changed = lambda e: on_select_changed(backup, index) self.on_long_press = lambda e: on_long_press(backup) self.cells = [ @@ -18,6 +22,6 @@ def __init__( ft.Text(backup.name), on_tap=lambda e: open_with_explorer(backup.path) ), ft.DataCell(ft.Text(backup.title)), - ft.DataCell(ft.Text(convert_timestamp(backup.created))), - ft.DataCell(ft.Checkbox(value=backup.pull_ignore, disabled=True)), + ft.DataCell(ft.Text(convert_timestamp(backup.created, UI_DATETIME_FORMAT))), + ft.DataCell(ft.Checkbox(value=backup.pool_ingore, disabled=True)), ] diff --git a/data/controls/backup_entry.py b/data/controls/backup_entry.py index b52bb88..53309db 100644 --- a/data/controls/backup_entry.py +++ b/data/controls/backup_entry.py @@ -8,17 +8,20 @@ class BackupEntry(ft.Column): - def __init__(self, backup: Backup): + def __init__(self, backup: Backup, disable_name_field: bool = False): super().__init__(expand=True) self.result = backup self.name_field = ft.TextField( - label="File name", value=backup.name, expand=True + label="File name", + value=backup.name, + expand=True, + disabled=disable_name_field, ) self.title_field = ft.TextField(label="Title", value=backup.title, expand=True) - self.pull_ignore_checkbox = ft.Checkbox( - label="Pull ignore", value=backup.pull_ignore + self.pool_ignore_checkbox = ft.Checkbox( + label="Pool ignore", value=backup.pool_ingore ) creation_time = datetime.fromtimestamp(self.result.created) self.time_picker = TimePicker(creation_time) @@ -27,7 +30,7 @@ def __init__(self, backup: Backup): self.controls = [ self.name_field, self.title_field, - self.pull_ignore_checkbox, + self.pool_ignore_checkbox, self.time_picker, self.date_picker, ] @@ -35,7 +38,7 @@ def __init__(self, backup: Backup): def get_backup(self) -> Backup: self.result.name = self.name_field.value # type: ignore self.result.title = self.title_field.value # type: ignore - self.result.pull_ignore = self.pull_ignore_checkbox.value # type: ignore + self.result.pool_ingore = self.pool_ignore_checkbox.value # type: ignore self.result.created = int( datetime.combine(self.date_picker.get(), self.time_picker.get()).timestamp() ) diff --git a/data/controls/backups_editor.py b/data/controls/backups_editor.py index 072f49c..e1a5b88 100644 --- a/data/controls/backups_editor.py +++ b/data/controls/backups_editor.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Optional from pathlib import Path import flet as ft @@ -14,9 +14,11 @@ class BackupsEditor(ft.Column): def __init__(self, page: ft.Page) -> None: super().__init__() self.page: ft.Page = page + self.expand = True + self.scroll = ft.ScrollMode.AUTO self.backup_view = BackupsView( - self._handle_selection_change, self._handle_long_press + self._handle_selection_change, self._show_change_backup_menu ) self.backup_manager = BackupManager() @@ -28,19 +30,31 @@ def __init__(self, page: ft.Page) -> None: icon=ft.icons.ADD, on_click=self._show_create_popup, ) + self.edit_button = ft.TextButton( + "Edit", + icon=ft.icons.EDIT, + on_click=lambda e: self._show_change_backup_menu(), + ) self.delete_button = ft.TextButton( "Delete", icon=ft.icons.DELETE, on_click=self._delete_handler ) self.disable_all_buttons(True) self.controls = [ - ft.Row([self.restore_button, self.create_button, self.delete_button]), + ft.Row( + [ + self.restore_button, + self.create_button, + self.edit_button, + self.delete_button, + ] + ), self.backup_view, ] self.current_world: McWorld = McWorld(Path()) self.selected_backup: Union[None, Backup] = None def update_backup_view(self) -> None: - self.backup_view.set_backups(self.backup_manager.backups) + self.backup_view.set_backups(self.backup_manager.get_sorted_backups()) self.disable_control_buttons(True) self.update() @@ -51,6 +65,7 @@ def disable_all_buttons(self, state: bool) -> None: def disable_control_buttons(self, state: bool) -> None: self.restore_button.disabled = state self.delete_button.disabled = state + self.edit_button.disabled = state def change_world(self, new_world: McWorld) -> None: self.current_world = new_world @@ -63,11 +78,16 @@ def _handle_selection_change(self, selected: Backup) -> None: self.disable_control_buttons(False) self.update() - def _handle_long_press(self, backup: Backup) -> None: + def _show_change_backup_menu(self, backup: Optional[Backup] = None) -> None: + if backup is None: + if self.selected_backup is None: + return + backup = self.selected_backup + ChangeBackup(self.page, backup, self._change_backup_handler).show() def _change_backup_handler(self, changed_backup: Backup): - self.backup_manager.update(changed_backup) + self.backup_manager.save() self.update_backup_view() def _delete_handler(self, e) -> None: diff --git a/data/controls/backups_view.py b/data/controls/backups_view.py index 31cf3b2..49cf79b 100644 --- a/data/controls/backups_view.py +++ b/data/controls/backups_view.py @@ -1,14 +1,16 @@ -from typing import Callable +from functools import partial +from typing import Callable, Optional import flet as ft -from data.backup_manager import Backup +from data.backup_manager import Backup, sort_backups, SortKeys from data.controls.backup_data_row import BackupDataRow class BackupsView(ft.DataTable): def __init__(self, on_select_changed: Callable, on_long_press: Callable) -> None: super().__init__() + self.selected_row = None self.border = ft.Border( ft.BorderSide(1), @@ -17,24 +19,56 @@ def __init__(self, on_select_changed: Callable, on_long_press: Callable) -> None ft.BorderSide(1), ) self.columns = [ - ft.DataColumn(ft.Text("File name")), - ft.DataColumn(ft.Text("Title")), - ft.DataColumn(ft.Text("Created(Y.M.D H-M-S)")), - ft.DataColumn(ft.Text("Pull ignore")), + ft.DataColumn( + ft.Text("File name"), + on_sort=partial(self.sort_table, sort_key=SortKeys.NAME), + ), + ft.DataColumn( + ft.Text("Title"), + on_sort=partial(self.sort_table, sort_key=SortKeys.TITLE), + ), + ft.DataColumn( + ft.Text("Created(Y.M.D H-M-S)"), + on_sort=partial(self.sort_table, sort_key=SortKeys.CREATED), + ), + ft.DataColumn( + ft.Text("Pool ignore"), + on_sort=partial(self.sort_table, sort_key=SortKeys.POOL_IGNORE), + ), ] self.rows = [] self.backups: list[Backup] = [] self.on_select_changed = on_select_changed self.on_long_press = on_long_press + def sort_table(self, e, sort_key: SortKeys): + e.control.data = not e.control.data + self.backups = sort_backups(self.backups, sort_key, e.control.data) + self.update_table() + def set_backups(self, backups: list[Backup]) -> None: self.backups = backups self.update_table() def update_table(self) -> None: + self.set_selected_row(None) self.rows.clear() - for backup in self.backups: + for index, backup in enumerate(self.backups): self.rows.append( - BackupDataRow(backup, self.on_select_changed, self.on_long_press) + BackupDataRow( + backup, self._handle_select_change, self.on_long_press, index + ) ) self.update() + + def set_selected_row(self, row_index: Optional[int]) -> None: + if self.selected_row is not None: + self.rows[self.selected_row].selected = False + self.selected_row = row_index + if self.selected_row is not None: + self.rows[self.selected_row].selected = True + + def _handle_select_change(self, backup: Backup, index: int) -> None: + self.on_select_changed(backup) + self.set_selected_row(index) + self.update() diff --git a/data/controls/world_view.py b/data/controls/world_view.py index f5045b7..b72e53c 100644 --- a/data/controls/world_view.py +++ b/data/controls/world_view.py @@ -9,6 +9,7 @@ class WorldView(ft.Column): def __init__(self, page: ft.Page) -> None: super().__init__() + self.expand = True self.page = page self.world_name_text = ft.Text(weight=ft.FontWeight.BOLD, size=20) self.world_path_text = ClickableText( diff --git a/data/dialogs/change_backup.py b/data/dialogs/change_backup.py index 3627616..66d9da3 100644 --- a/data/dialogs/change_backup.py +++ b/data/dialogs/change_backup.py @@ -12,7 +12,7 @@ def __init__( self, page: ft.Page, backup: Backup, after_completion: Callable ) -> None: super().__init__(page=page, title="Change backup") - self.backup_entry = BackupEntry(backup) + self.backup_entry = BackupEntry(backup, True) self.content = self.backup_entry diff --git a/data/dialogs/change_settings.py b/data/dialogs/change_settings.py index c38d67b..5fdd9e5 100644 --- a/data/dialogs/change_settings.py +++ b/data/dialogs/change_settings.py @@ -13,10 +13,10 @@ def __init__(self, page: ft.Page) -> None: super().__init__(page=page, title="Settings") self.backups_folder_field = PathField(label="Backups folder", page=page) self.mc_folder_field = PathField(label="Minecraft folder", page=page) - self.pull_size_entry = ft.TextField(label="Pull size") + self.pool_size_entry = ft.TextField(label="Pool size") self.content = ft.Column( - [self.backups_folder_field, self.mc_folder_field, self.pull_size_entry] + [self.backups_folder_field, self.mc_folder_field, self.pool_size_entry] ) self.actions = [ ft.TextButton("Save", on_click=self.save), @@ -30,7 +30,7 @@ def save(self, event=None) -> None: if not self._validate_mc_folder_path(): return - if not self._validate_pull_size(): + if not self._validate_pool_size(): return self.close() @@ -47,8 +47,8 @@ def _update_fields_value(self): self.mc_folder_field.path = str(Settings().get_mc_folder()) self.backups_folder_field.remove_error() - self.pull_size_entry.value = str(Settings().get_pull_size()) - self.pull_size_entry.error_text = "" + self.pool_size_entry.value = str(Settings().get_pool_size()) + self.pool_size_entry.error_text = "" def _validate_backup_folder_path(self) -> bool: backup_folder = Path(self.backups_folder_field.path) # type: ignore @@ -68,16 +68,16 @@ def _validate_mc_folder_path(self) -> bool: self.mc_folder_field.remove_error() return True - def _validate_pull_size(self) -> bool: + def _validate_pool_size(self) -> bool: try: - pull_size = int(self.pull_size_entry.value) # type: ignore - if pull_size <= 0: + pool_size = int(self.pool_size_entry.value) # type: ignore + if pool_size <= 0: raise ValueError() except ValueError: - self.pull_size_entry.error_text = "Wrong size" - self.pull_size_entry.update() + self.pool_size_entry.error_text = "Wrong size" + self.pool_size_entry.update() return False - self.pull_size_entry.error_text = "" - self.pull_size_entry.update() - Settings().update_pull_size(pull_size) + self.pool_size_entry.error_text = "" + self.pool_size_entry.update() + Settings().update_pool_size(pool_size) return True diff --git a/data/path_utils.py b/data/path_utils.py index 8880187..6116e0d 100644 --- a/data/path_utils.py +++ b/data/path_utils.py @@ -3,12 +3,19 @@ from data.settings import Settings -def get_rel_path(path: Path) -> Path: - return path.relative_to(Settings().get_mc_folder()) +def get_rel_world_path(world_path: Path) -> Path: + """Return relative path to Minecraft folder. It is used to remove `.minecraft` from world path. + Example `.minecraft/saves/NewWorld` converts to `saves/NewWorld` + """ -def convert_world_path_to_backup(path: Path) -> Path: - return Settings().get_backup_folder().joinpath(get_rel_path(path)) + return world_path.relative_to(Settings().get_mc_folder()) + + +def convert_world_path_to_backup(world_path: Path) -> Path: + """Convert world path(example `.minecraft/saves/NewWorld`) to path for backup folder(example `backups/saves/NewWrold`).""" + + return Settings().get_backup_folder().joinpath(get_rel_world_path(world_path)) def get_top_dir(path: Path) -> Path: diff --git a/data/settings.py b/data/settings.py index f5347b0..51db717 100644 --- a/data/settings.py +++ b/data/settings.py @@ -6,7 +6,7 @@ SETTINGS_FILE = Path("settings.json") -DEFAULT_PULL_SIZE = 4 +DEFAULT_POOL_SIZE = 4 DEFAULT_MC_FOLDER = get_default_mc_folder() DEFAULT_BACKUPS_FOLDER = Path("backups") @@ -34,7 +34,7 @@ def load(self) -> None: except OSError: self.update_backup_folder(DEFAULT_BACKUPS_FOLDER) self.update_mc_folder(DEFAULT_MC_FOLDER) - self.update_pull_size(DEFAULT_PULL_SIZE) + self.update_pool_size(DEFAULT_POOL_SIZE) def save(self) -> None: with open( @@ -62,11 +62,14 @@ def update_mc_folder(self, new_folder: Union[str, Path]) -> None: self._data["mc_folder"] = str(new_folder) self.save() - def get_pull_size(self) -> int: - if "pull_size" not in self._data: - self.update_pull_size(DEFAULT_PULL_SIZE) - return self._data["pull_size"] + def get_pool_size(self) -> int: + if "pull_size" in self._data: # Compatibility with previous versions + self._data["pool_size"] = self._data.pop("pull_size") - def update_pull_size(self, new_size: int): - self._data["pull_size"] = new_size + if "pool_size" not in self._data: + self.update_pool_size(DEFAULT_POOL_SIZE) + return self._data["pool_size"] + + def update_pool_size(self, new_size: int): + self._data["pool_size"] = new_size self.save() diff --git a/data/utils.py b/data/utils.py index f93aefc..fa42737 100644 --- a/data/utils.py +++ b/data/utils.py @@ -3,9 +3,9 @@ from datetime import datetime from pathlib import Path -DATE_FORMAT = "%Y.%m.%d" -TIME_FORMAT = "%H-%M-%S" -DATETIME_FORMAT = f"{DATE_FORMAT} {TIME_FORMAT}" + +FILE_DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S" +UI_DATETIME_FORMAT = "%Y.%m.%d %H:%M:%S" def open_with_explorer(path: Path) -> None: @@ -17,8 +17,9 @@ def open_with_explorer(path: Path) -> None: subprocess.Popen(["xdg-open", path]) -def convert_timestamp(timestamp: int) -> str: - return datetime.fromtimestamp(timestamp).strftime(DATETIME_FORMAT) +def convert_timestamp(timestamp: int, time_format: str) -> str: + return datetime.fromtimestamp(timestamp).strftime(time_format) + def get_default_mc_folder() -> Path: base_path = Path().home()