Skip to content

Commit

Permalink
v.1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
AntynK committed Jun 22, 2024
1 parent 6e51730 commit f43153c
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 85 deletions.
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
Empty file added CONTRIBUTORS.md
Empty file.
21 changes: 21 additions & 0 deletions LICENCE
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 `<backups>/saves/<WorldName>/backups`. From the `versions` folder at `<backups>/versions/<VersionName>/saves/<WorldName>/backups`.

`<backups>` - the folder where all backups are saved (can be changed in settings).
`<VersionName>` - version name.
`<WorldName>` - 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).
55 changes: 55 additions & 0 deletions README_UA.md
Original file line number Diff line number Diff line change
@@ -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` зберігаються у папці `<backups>/saves/<WorldName>/backups`, а з папки `versions` у `<backups>/versions/<VersionName>/saves/<WorldName>/backups`.

`<backups>` - папка, де зберігаються всі резервні копії (її можна змінити в налаштуваннях).
`<VersionName>` - назва версії.
`<WorldName>` - назва світу.
`backups` - папка, де зберігаються резервні копії світу.

> [!NOTE]
> Назви беруться з назв папок, тому вони можуть відрізнятися від того, як ви їх бачите в грі.
> [!TIP]
> У меню вибору світу під назвою світу є назва папки, також під час редагування світу можна відкрити його папку.
## Особливість
Програма може автоматично визначати дату створення резервної копії (якщо вона є в назві і має формат `РІК-МІСЯЦЬ-ДЕНЬ_ГОДИНА-ХВИЛИНА-СЕКУНДА`), тому ви можете перенести резервні копії, створені за допомогою Minecraft.
> [!NOTE]
> Копії, які були перенесені таким чином, входитимуть в пул.

## Контриб'ютори
Якщо у вас є ідеї щодо покращення чи бажання долучитися до розвитку проєкту, будь ласка, подайте свій внесок. Відкрийте [CONTRIBUTING.md](CONTRIBUTING.md) для детального ознайомлення.
87 changes: 59 additions & 28 deletions data/backup_manager.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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):
Expand All @@ -32,25 +34,41 @@ 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] = []
self.work_dir = Path()
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):
Expand All @@ -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"
)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -111,32 +142,32 @@ 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:
result = 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
14 changes: 9 additions & 5 deletions data/controls/backup_data_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
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 = [
ft.DataCell(
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)),
]
15 changes: 9 additions & 6 deletions data/controls/backup_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -27,15 +30,15 @@ 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,
]

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()
)
Expand Down
Loading

0 comments on commit f43153c

Please sign in to comment.