Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
zyxkad committed Jan 6, 2022
1 parent db7ec32 commit 8caa8b4
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 24 deletions.
2 changes: 1 addition & 1 deletion mcdreforged.plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "smart_backup",
"version": "0.0.1",
"version": "1.0.0",
"name": "SmartBackup",
"description": "A Minecraft Backup Plugin",
"author": "zyxkad",
Expand Down
1 change: 1 addition & 0 deletions smart_backup/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

97 changes: 86 additions & 11 deletions smart_backup/commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

import os
import time

import mcdreforged.api.all as MCDR
from .utils import *
from . import globals as GL
Expand All @@ -12,7 +15,7 @@
{0} query <id> 查询备份详细信息
{0} make [<comment>] 创建新备份(差异/全盘)
{0} makefull [<comment>] 创建全盘备份
{0} rm <id> [force] 删除指定备份(及其子备份)
{0} rm <id> [force] 删除指定备份(及其子备份) #TODO
{0} restore [<id>] 回档至[上次/指定id]备份
{0} confirm 确认操作
{0} abort 取消操作
Expand All @@ -36,17 +39,17 @@ def register(server: MCDR.PluginServerInterface):
then(GL.Config.literal('help').runs(command_help)).
then(GL.Config.literal('list').
runs(lambda src: command_list_backup(src, 10)).
then(MCDR.Integer('limit').at_min(0).
runs(lambda src, ctx: command_list_backup(src, ctx['limit'])))).
then(MCDR.Integer('limit').at_min(0).runs(lambda src, ctx: command_list_backup(src, ctx['limit'])))).
then(GL.Config.literal('query').
then(MCDR.Text('id').
runs(lambda src, ctx: command_query_backup(src, ctx['id'])))).
then(MCDR.Text('id').runs(lambda src, ctx: command_query_backup(src, ctx['id'])))).
then(GL.Config.literal('make').
runs(lambda src: command_make(src, 'None')).
then(MCDR.GreedyText('comment').runs(lambda src, ctx: command_make(src, ctx['comment'])))).
then(GL.Config.literal('makefull').
runs(lambda src: command_makefull(src, 'None')).
then(MCDR.GreedyText('comment').runs(lambda src, ctx: command_makefull(src, ctx['comment'])))).
then(GL.Config.literal('restore').
then(MCDR.Text('id').runs(lambda src, ctx: command_restore(src, ctx['id'])))).
then(GL.Config.literal('confirm').runs(command_confirm)).
then(GL.Config.literal('abort').runs(command_abort)).
then(GL.Config.literal('reload').runs(command_config_load)).
Expand All @@ -58,12 +61,30 @@ def command_help(source: MCDR.CommandSource):

def command_list_backup(source: MCDR.CommandSource, limit: int):
bks = Backup.list(GL.Config.backup_path, limit)
lines = [MCDR.RText(b.z_index * '|' + hex(b.timestamp) + ': ' + b.comment) for b in bks]
lines = [MCDR.RTextList(b.z_index * '|',
new_command('{0} restore {1}'.format(Prefix, hex(b.timestamp)), hex(b.timestamp)),
': ' + b.comment) for b in bks]
send_block_message(source, *lines)

@new_thread
def command_query_backup(source: MCDR.CommandSource, bid: str):
send_message(source, 'TODO: query backup "{}"'.format(bid))
if not bid.startswith('0x'):
bid = '0x' + bid
path = os.path.join(GL.Config.backup_path, bid)
bk = Backup.load(path)
if bk is None:
send_message(source, MCDR.RText('Cannot find backup with id "{}"'.format(bid)))
return
bk_size = get_total_size(path)
send_block_message(source,
'ID: ' + hex(bk.timestamp),
'Comment: ' + bk.comment,
'Date: ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(bk.timestamp / 1000)),
'Size: ' + format_size(bk_size),
join_rtext(
new_command('{0} restore {1}'.format(Prefix, hex(bk.timestamp)), '[回档]')
)
)

@new_thread
@new_job('make backup')
Expand All @@ -73,14 +94,19 @@ def command_make(source: MCDR.CommandSource, comment: str):
tuple(map(server.execute, GL.Config.befor_backup))

def call():
mode = BackupMode.FULL
prev: Backup = None
mode: BackupMode = BackupMode.FULL
if 'differential_count' not in GL.Config.cache or GL.Config.cache['differential_count'] >= GL.Config.differential_backup_limit:
GL.Config.cache['differential_count'] = 0
else:
mode = BackupMode.DIFFERENTIAL
GL.Config.cache['differential_count'] += 1
prev = Backup.get_last(GL.Config.backup_path)
if prev is None:
GL.Config.cache['differential_count'] = 0
else:
GL.Config.cache['differential_count'] += 1
mode = BackupMode.DIFFERENTIAL
backup = Backup.create(mode, comment,
source.get_server().get_mcdr_config()['working_directory'], GL.Config.backup_needs, GL.Config.backup_ignores)
source.get_server().get_mcdr_config()['working_directory'], GL.Config.backup_needs, GL.Config.backup_ignores, prev=prev)
tuple(map(server.execute, GL.Config.after_backup))
send_message(source, 'Saving backup "{}"'.format(comment), log=True)
backup.save(GL.Config.backup_path)
Expand Down Expand Up @@ -114,6 +140,55 @@ def call():
send_message(source, 'Making full backup "{}"'.format(comment), log=True)
tuple(map(server.execute, GL.Config.befor_backup))

@new_thread
@new_job('restore')
def command_restore(source: MCDR.CommandSource, bid: str):
if not bid.startswith('0x'):
bid = '0x' + bid
path = os.path.join(GL.Config.backup_path, bid)
bk = Backup.load(path)
if bk is None:
send_message(source, MCDR.RText('Cannot find backup with id "{}"'.format(bid)))
return
server = source.get_server()

def restore():
abort: bool = False
timeout: int = GL.Config.restore_timeout
def ab():
nonlocal abort
abort = True
date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(bk.timestamp / 1000))
register_confirm(None, lambda:0, ab)
while timeout > 0:
broadcast_message('{t} 秒后将重启回档至{date}({comment}), 输入'.format(t=timeout, date=date, comment=bk.comment),
new_command('{} abort'.format(Prefix)), '撤销回档')
time.sleep(1)
if abort:
broadcast_message('已取消回档')
return
timeout -= 1
confirm_map.pop(None, None)

broadcast_message('Stopping the server')
server.stop()
server.wait_for_start()
log_info('Restoring...')
bk.restore(server.get_mcdr_config()['working_directory'], GL.Config.backup_needs, GL.Config.backup_ignores)
log_info('Starting the server')
server.start()

begin_job()
register_confirm(source.player if source.is_player else '',
new_thread(lambda: (restore(), after_job())),
lambda: (send_message(source, '已取消回档'), after_job()))
send_message(source, MCDR.RText('确认回档至 "{}" 吗?'.format(bk.comment)).
h('id: ' + hex(bk.timestamp),
'comment: ' + bk.comment,
'date: ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(bk.timestamp / 1000)),
'size: ' + format_size(get_total_size(path))))
send_message(source, '输入', new_command('{} confirm'.format(Prefix)), '确认, 输入',
new_command('{} abort'.format(Prefix)), '取消')

def command_confirm(source: MCDR.CommandSource):
confirm_map.pop(source.player if source.is_player else '', (lambda s: send_message(s, '当前没有正在执行的操作'), 0))[0](source)
Expand Down
20 changes: 14 additions & 6 deletions smart_backup/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,14 @@ def get(self, base, *path):
return f

@classmethod
def create(cls, mode: BackupMode, comment: str, base: str, needs: list, ignores: list = []):
def create(cls, mode: BackupMode, comment: str, base: str, needs: list, ignores: list = [], prev: Backup = None):
timestamp: int = int(time.time() * 1000)
files: list = []
prev: Backup = None
l = set(os.listdir(base))
if mode != BackupMode.FULL:
prev = cls.get_last()
if mode == BackupMode.FULL:
if prev is not None:
prev = None
else:
assert prev is not None
if mode == BackupMode.DIFFERENTIAL:
while prev.mode != BackupMode.FULL:
Expand Down Expand Up @@ -330,6 +331,8 @@ def load(cls, path: str):
rpath = os.path.realpath(path)
if rpath in cls._cache:
return cls._cache[rpath]
if not os.path.exists(path):
return None
mode: BackupMode
timestamp: int = int(os.path.basename(path), 16)
comment: str
Expand Down Expand Up @@ -358,13 +361,17 @@ def __hash__(self):

@staticmethod
def list(path: str, limit: int = -1):
if not os.path.exists(path):
return list()
ids = sorted(map(lambda a: int(a, 16), filter(lambda a: a.startswith('0x'), os.listdir(path))))
if limit > 0:
ids = ids[-limit:]
return [Backup.load(os.path.join(path, hex(i))) for i in ids]

@staticmethod
def get_last(path: str):
if not os.path.exists(path):
return None
ids = sorted(map(lambda a: int(a, 16), filter(lambda a: a.startswith('0x'), os.listdir(path))))
if len(ids) == 0:
return None
Expand All @@ -384,11 +391,12 @@ def call(path: str):
return call

def filters(ignores: list):
ignorec = []
for i, s in enumerate(ignores):
ignores[i] = _filter(s)
ignorec.append(_filter(s))

def call(path: str):
for c in ignores:
for c in ignorec:
s = c(path)
if s == 1:
return False
Expand Down
52 changes: 46 additions & 6 deletions smart_backup/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import os
from threading import RLock, Condition
import functools

Expand All @@ -7,11 +8,13 @@

__all__ = [
'new_thread', 'get_current_job', 'begin_job', 'after_job', 'new_job',
'join_rtext', 'send_block_message', 'send_message', 'broadcast_message', 'log_info'
'new_command', 'join_rtext', 'send_block_message', 'send_message', 'broadcast_message', 'log_info',
'get_total_size', 'format_size'
]

def new_thread(call):
@MCDR.new_thread('smart_backup')
@functools.wraps(call)
def c(*args, **kwargs):
return call(*args, **kwargs)
return c
Expand All @@ -23,20 +26,20 @@ def get_current_job():
with job_lock:
return None if current_job is None else current_job[0]

def check_job(job: str):
def check_job():
with job_lock:
return current_job is None or current_job[0] == job
return current_job is None

def begin_job(job: str = None, block=False):
global current_job, job_lock
if block or job is None or current_job is None or current_job[0] == job:
if block or job is None or current_job is None:
with job_lock:
while True:
if current_job is None:
assert job is not None
current_job = [job, 1]
return True
if job is None or current_job[0] == job:
if job is None:
current_job[1] += 1
return True
if not block:
Expand All @@ -58,7 +61,7 @@ def w(call):
@functools.wraps(call)
def c(*args, **kwargs):
with job_lock:
if not check_job(job) and len(args) > 0 and isinstance(args[0], MCDR.CommandSource):
if not check_job() and len(args) > 0 and isinstance(args[0], MCDR.CommandSource):
send_message(args[0], MCDR.RText('In progress {} now'.format(current_job[0]), color=MCDR.RColor.red))
return None
else:
Expand All @@ -70,6 +73,15 @@ def c(*args, **kwargs):
return c
return w

def new_command(cmd: str, text=None, **kwargs):
if text is None:
text = cmd
if 'color' not in kwargs:
kwargs['color'] = MCDR.RColor.yellow
if 'styles' not in kwargs:
kwargs['styles'] = MCDR.RStyle.underlined
return MCDR.RText(text, **kwargs).c(MCDR.RAction.run_command, cmd)

def join_rtext(*args, sep=' '):
return MCDR.RTextList(args[0], *(MCDR.RTextList(sep, a) for a in args[1:]))

Expand All @@ -95,3 +107,31 @@ def broadcast_message(*args, sep=' ', prefix=GL.MSG_ID):
def log_info(*args, sep=' ', prefix=GL.MSG_ID):
if GL.SERVER_INS is not None:
GL.SERVER_INS.logger.info(join_rtext(prefix, *args, sep=sep))

def get_total_size(path: str):
size = 0
for root, _, files in os.walk(path):
for f in files:
f = os.path.join(root, f)
size += os.stat(f).st_size
return size

def format_size(size: int):
sz: float = float(size)
ut: str = 'B'
if sz >= 1000:
sz /= 1024
ut = 'KB'
if sz >= 1000:
sz /= 1024
ut = 'MB'
if sz >= 1000:
sz /= 1024
ut = 'GB'
if sz >= 1000:
sz /= 1024
ut = 'TB'
if sz >= 1000:
sz /= 1024
ut = 'PB'
return '{0:.2f}{1}'.format(sz, ut)

0 comments on commit 8caa8b4

Please sign in to comment.