33import os
44import re
55import shutil
6+ import tarfile
67import time
8+ from enum import Enum , auto
79from threading import Lock
810from typing import Optional , Any , Callable , Tuple
911
2426operation_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+
2751def 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
71135def 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]]:
162224def 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 = '' )
0 commit comments