Skip to content

Commit 86fa53c

Browse files
committed
backup_format support
tar backup!
1 parent 361d576 commit 86fa53c

4 files changed

Lines changed: 136 additions & 38 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,22 @@ mcd_root/
194194
...
195195
```
196196

197-
执行`!!qb back`时,会从备份槽中指定世界名对应的符号链接开始,将所有符号链接以及最终实际的世界文件夹恢复至服务端的对应位置。这表示如果后续服务端的符号链接更改了指向的世界,回档时将恢复到备份时保存的世界,且不同世界的内容不会互相覆盖
197+
执行 `!!qb back` 时,会从备份槽中指定世界名对应的符号链接开始,将所有符号链接以及最终实际的世界文件夹恢复至服务端的对应位置。这表示如果后续服务端的符号链接更改了指向的世界,回档时将恢复到备份时保存的世界,且不同世界的内容不会互相覆盖
198+
199+
### backup_format
200+
201+
备份的储存格式
202+
203+
|| 含义 |
204+
|----------|-------------------------------------------------------------------|
205+
| `plain` | 直接复制文件夹/文件来储存。默认值,这同时也是 v1.8 以前版本的 QBM 唯一支持的储存格式 |
206+
| `tar` | 使用 tar 格式直接打包储存至 `backup.tar` 文件中。推荐使用,可有效减少文件的数量,但无法方便地访问备份里面的文件 |
207+
| `tar_gz` | 使用 tar.gz 格式压缩打包储存至 `backup.tar.gz` 文件中。能减小备份体积,但是备份/回档的耗时将极大增加 |
208+
209+
槽位的备份模式会储存在槽位的 `info.json` 中,并在回档时读取,因此的不同的槽位可以有着不同的储存格式。
210+
若其值不存在,QBM 会假定这个槽位是由旧版 QBM 创建的,并使用默认值 `plain`
211+
212+
若配置文件中的 `backup_format` 非法,则会使用默认值 `plain`
198213

199214
### minimum_permission_level
200215

README_en.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ mcd_root/
197197

198198
Doing `!!qb back` will restore everything from world name symlink to the final actual world folder in the slot to the server's corresponding place. This implies that if the symlink has changed its target world, the server will be restored to the world when making backup, and the world before restoring will not be overwritten
199199

200+
### backup_format
201+
202+
The format of the stored backup
203+
204+
| Value | Explanation |
205+
|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
206+
| `plain` | Store the backup directly via file / directory copy. The default value, the only supported format in QBM < v1.8 |
207+
| `tar` | Pack the files into `backup.tar` in tar format. Recommend value. It can significantly reduce the file amount. Although you cannot access files inside the backup easily |
208+
| `tar_gz` | Compress the files into `backup.tar.gz` in tar.gz format. The backup size will be smaller, but the time cost in backup / restore will increase quite a lot |
209+
210+
槽位的备份模式会储存在槽位的 `info.json` 中,并在回档时读取。若其值不存在,则使用默认值 `plain`,对应着旧版 QBM 的表现
211+
212+
The backup format of the slot will be stored inside the `info.json` of the slot, and will be read when restoring, so you can have different backup formats in your slots.
213+
If the backup format value doesn't exist, QBM will assume that it's a backup created from old QBM, and use the default `plain` format
214+
215+
If the `backup_format` value is invalid in the config file, the default value `plain` will be used
216+
200217
### minimum_permission_level
201218

202219
Default:

quick_backup_multi/__init__.py

Lines changed: 102 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import os
44
import re
55
import shutil
6+
import tarfile
67
import time
8+
from enum import Enum, auto
79
from threading import Lock
810
from typing import Optional, Any, Callable, Tuple
911

@@ -24,6 +26,28 @@
2426
operation_name = RText('?')
2527

2628

29+
class CopyWorldIntent(Enum):
30+
backup = auto()
31+
restore = auto()
32+
33+
34+
class BackupFormat(Enum):
35+
plain = auto()
36+
tar = auto()
37+
tar_gz = auto()
38+
39+
@classmethod
40+
def of(cls, mode: str) -> 'BackupFormat':
41+
try:
42+
return cls[mode]
43+
except Exception:
44+
return cls.plain
45+
46+
47+
def get_backup_format() -> BackupFormat:
48+
return BackupFormat.of(config.backup_format)
49+
50+
2751
def tr(translation_key: str, *args) -> RTextMCDRTranslation:
2852
return ServerInterface.get_instance().rtr('quick_backup_multi.{}'.format(translation_key), *args)
2953

@@ -41,31 +65,71 @@ def command_run(message: Any, text: Any, command: str) -> RTextBase:
4165
return fancy_text.set_hover_text(text).set_click_event(RAction.run_command, command)
4266

4367

44-
def copy_worlds(src: str, dst: str):
45-
for world in config.world_names:
46-
src_path = os.path.join(src, world)
47-
dst_path = os.path.join(dst, world)
48-
49-
while os.path.islink(src_path):
50-
server_inst.logger.info('copying {} -> {} (symbolic link)'.format(src_path, dst_path))
51-
dst_dir = os.path.dirname(dst_path)
52-
if not os.path.isdir(dst_dir):
53-
os.makedirs(dst_dir)
54-
link_path = os.readlink(src_path)
55-
os.symlink(link_path, dst_path)
56-
src_path = link_path if os.path.isabs(link_path) else os.path.normpath(os.path.join(os.path.dirname(src_path), link_path))
57-
dst_path = os.path.join(dst, os.path.relpath(src_path, src))
58-
59-
server_inst.logger.info('copying {} -> {}'.format(src_path, dst_path))
60-
if os.path.isdir(src_path):
61-
shutil.copytree(src_path, dst_path, ignore=lambda path, files: set(filter(config.is_file_ignored, files)))
62-
elif os.path.isfile(src_path):
63-
dst_dir = os.path.dirname(dst_path)
64-
if not os.path.isdir(dst_dir):
65-
os.makedirs(dst_dir)
66-
shutil.copy(src_path, dst_path)
67-
else:
68-
server_inst.logger.warning('{} does not exist while copying ({} -> {})'.format(src_path, src_path, dst_path))
68+
def get_backup_file_name(backup_format: BackupFormat):
69+
if backup_format == BackupFormat.plain:
70+
raise ValueError('plain mode is not supported')
71+
elif backup_format == BackupFormat.tar:
72+
return 'backup.tar'
73+
elif backup_format == BackupFormat.tar_gz:
74+
return 'backup.tar.gz'
75+
else:
76+
raise ValueError('unknown backup mode {}'.format(backup_format))
77+
78+
79+
def copy_worlds(src: str, dst: str, intent: CopyWorldIntent, *, backup_format: Optional[BackupFormat] = None):
80+
if backup_format is None:
81+
backup_format = get_backup_format()
82+
if backup_format == BackupFormat.plain:
83+
for world in config.world_names:
84+
src_path = os.path.join(src, world)
85+
dst_path = os.path.join(dst, world)
86+
87+
while os.path.islink(src_path):
88+
server_inst.logger.info('copying {} -> {} (symbolic link)'.format(src_path, dst_path))
89+
dst_dir = os.path.dirname(dst_path)
90+
if not os.path.isdir(dst_dir):
91+
os.makedirs(dst_dir)
92+
link_path = os.readlink(src_path)
93+
os.symlink(link_path, dst_path)
94+
src_path = link_path if os.path.isabs(link_path) else os.path.normpath(os.path.join(os.path.dirname(src_path), link_path))
95+
dst_path = os.path.join(dst, os.path.relpath(src_path, src))
96+
97+
server_inst.logger.info('copying {} -> {}'.format(src_path, dst_path))
98+
if os.path.isdir(src_path):
99+
shutil.copytree(src_path, dst_path, ignore=lambda path, files: set(filter(config.is_file_ignored, files)))
100+
elif os.path.isfile(src_path):
101+
dst_dir = os.path.dirname(dst_path)
102+
if not os.path.isdir(dst_dir):
103+
os.makedirs(dst_dir)
104+
shutil.copy(src_path, dst_path)
105+
else:
106+
server_inst.logger.warning('{} does not exist while copying ({} -> {})'.format(src_path, src_path, dst_path))
107+
elif backup_format == BackupFormat.tar or backup_format == BackupFormat.tar_gz:
108+
if intent == CopyWorldIntent.restore:
109+
tar_path = os.path.join(src, get_backup_file_name(backup_format))
110+
server_inst.logger.info('extracting {} -> {}'.format(tar_path, dst))
111+
with tarfile.open(tar_path, 'r:*') as backup_file:
112+
backup_file.extractall(path=dst)
113+
else: # backup
114+
if backup_format == BackupFormat.tar_gz:
115+
tar_mode = 'w:gz'
116+
else:
117+
tar_mode = 'w'
118+
if not os.path.isdir(dst):
119+
os.makedirs(dst)
120+
tar_path = os.path.join(dst, get_backup_file_name(backup_format))
121+
with tarfile.open(tar_path, tar_mode) as backup_file:
122+
for world in config.world_names:
123+
src_path = os.path.join(src, world)
124+
server_inst.logger.info('storing {} -> {}'.format(src_path, tar_path))
125+
if os.path.exists(src_path):
126+
def tar_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
127+
if config.is_file_ignored(info.name):
128+
return None
129+
return info
130+
backup_file.add(src_path, arcname=world, filter=tar_filter)
131+
else:
132+
server_inst.logger.warning('{} does not exist while storing'.format(src_path))
69133

70134

71135
def remove_worlds(folder: str):
@@ -122,11 +186,9 @@ def format_protection_time(time_length: float) -> RTextBase:
122186
return tr('day', round(time_length / 60 / 60 / 24, 2))
123187

124188

125-
def format_slot_info(info_dict: Optional[dict] = None, slot_number: Optional[int] = None) -> Optional[RTextBase]:
189+
def format_slot_info(info_dict: Optional[dict] = None) -> Optional[RTextBase]:
126190
if isinstance(info_dict, dict):
127191
info = info_dict
128-
elif slot_number is not None:
129-
info = get_slot_info(slot_number)
130192
else:
131193
return None
132194

@@ -162,7 +224,8 @@ def slot_check(source: CommandSource, slot: int) -> Optional[Tuple[int, dict]]:
162224
def create_slot_info(comment: Optional[str]) -> dict:
163225
slot_info = {
164226
'time': format_time(),
165-
'time_stamp': time.time()
227+
'time_stamp': time.time(),
228+
'backup_format': get_backup_format().name,
166229
}
167230
if comment is not None:
168231
slot_info['comment'] = comment
@@ -301,7 +364,7 @@ def _create_backup(source: CommandSource, comment: Optional[str]):
301364
slot_path = get_slot_path(1)
302365

303366
# copy worlds to backup slot
304-
copy_worlds(config.server_path, slot_path)
367+
copy_worlds(config.server_path, slot_path, CopyWorldIntent.backup)
305368

306369
# create info.json
307370
slot_info = create_slot_info(comment)
@@ -377,16 +440,17 @@ def _do_restore_backup(source: CommandSource, slot: int):
377440
overwrite_backup_path = os.path.join(config.backup_path, config.overwrite_backup_folder)
378441
if os.path.exists(overwrite_backup_path):
379442
shutil.rmtree(overwrite_backup_path)
380-
copy_worlds(config.server_path, overwrite_backup_path)
443+
copy_worlds(config.server_path, overwrite_backup_path, CopyWorldIntent.backup)
381444
with open(os.path.join(overwrite_backup_path, 'info.txt'), 'w') as f:
382445
f.write('Overwrite time: {}\n'.format(format_time()))
383446
f.write('Confirmed by: {}'.format(source))
384447

385448
slot_folder = get_slot_path(slot)
386449
server_inst.logger.info('Deleting world')
387450
remove_worlds(config.server_path)
388-
server_inst.logger.info('Restore backup ' + slot_folder)
389-
copy_worlds(slot_folder, config.server_path)
451+
backup_format = BackupFormat.of(slot_info.get('backup_format'))
452+
server_inst.logger.info('Restore backup {} (mode={})'.format(slot_folder, backup_format.name))
453+
copy_worlds(slot_folder, config.server_path, CopyWorldIntent.restore, backup_format=backup_format)
390454

391455
source.get_server().start()
392456
except:
@@ -423,7 +487,8 @@ def format_dir_size(size: int):
423487
backup_size = 0
424488
for i in range(get_slot_count()):
425489
slot_idx = i + 1
426-
slot_info = format_slot_info(slot_number=slot_idx)
490+
slot_info = get_slot_info(slot_idx)
491+
formatted_slot_info = format_slot_info(slot_info)
427492
if size_display:
428493
dir_size = get_dir_size(get_slot_path(slot_idx))
429494
else:
@@ -434,14 +499,14 @@ def format_dir_size(size: int):
434499
RText(tr('list_backup.slot.header', slot_idx)).h(tr('list_backup.slot.protection', format_protection_time(config.slots[slot_idx - 1].delete_protection))),
435500
' '
436501
)
437-
if slot_info is not None:
502+
if formatted_slot_info is not None:
438503
text += RTextList(
439504
RText('[▷] ', color=RColor.green).h(tr('list_backup.slot.restore', slot_idx)).c(RAction.run_command, f'{Prefix} back {slot_idx}'),
440505
RText('[×] ', color=RColor.red).h(tr('list_backup.slot.delete', slot_idx)).c(RAction.suggest_command, f'{Prefix} del {slot_idx}')
441506
)
442507
if size_display:
443-
text += '§2{}§r '.format(format_dir_size(dir_size))
444-
text += slot_info
508+
text += RText(format_dir_size(dir_size) + ' ', RColor.dark_green).h(BackupFormat.of(slot_info.get('backup_format')).name)
509+
text += formatted_slot_info
445510
print_message(source, text, prefix='')
446511
if size_display:
447512
print_message(source, tr('list_backup.total_space', format_dir_size(backup_size)), prefix='')

quick_backup_multi/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Configuration(Serializable):
2323
world_names: List[str] = [
2424
'world'
2525
]
26+
backup_format: str = 'plain' # "plain", "tar", "tar_gz"
2627
# 0:guest 1:user 2:helper 3:admin 4:owner
2728
minimum_permission_level: Dict[str, int] = {
2829
'make': 1,

0 commit comments

Comments
 (0)