From c7b4e8661c66a6c71da080180c3e54c77c64d2ed Mon Sep 17 00:00:00 2001 From: zyxkad Date: Wed, 5 Jan 2022 22:10:10 -0700 Subject: [PATCH] first commit --- .gitignore | 6 + mcdreforged.plugin.json | 11 ++ publisher.sh | 45 +++++ requirements.txt | 0 smart_backup/__init__.py | 20 ++ smart_backup/commands.py | 141 +++++++++++++ smart_backup/globals.py | 78 ++++++++ smart_backup/objects.py | 418 +++++++++++++++++++++++++++++++++++++++ smart_backup/utils.py | 97 +++++++++ 9 files changed, 816 insertions(+) create mode 100644 .gitignore create mode 100644 mcdreforged.plugin.json create mode 100755 publisher.sh create mode 100644 requirements.txt create mode 100644 smart_backup/__init__.py create mode 100644 smart_backup/commands.py create mode 100644 smart_backup/globals.py create mode 100644 smart_backup/objects.py create mode 100644 smart_backup/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bedbb0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +# Mac OS +.DS_Store + +# output +/output/ diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json new file mode 100644 index 0000000..7f8783f --- /dev/null +++ b/mcdreforged.plugin.json @@ -0,0 +1,11 @@ +{ + "id": "smart_backup", + "version": "0.0.1", + "name": "SmartBackup", + "description": "A Minecraft Backup Plugin", + "author": "zyxkad", + "link": "https://github.com/zyxkad/smart_backup_mcdr", + "dependencies": { + "mcdreforged": ">=2.0.0" + } +} \ No newline at end of file diff --git a/publisher.sh b/publisher.sh new file mode 100755 index 0000000..b6be806 --- /dev/null +++ b/publisher.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +COMMIT=true +RELEASE=true + +while [ -n "$1" ]; do + case $1 in + -n | --no-commit) + COMMIT='' + ;; + -R | --no-release) + RELEASE='' + ;; + esac + shift +done + +cd $(dirname $0) + +echo '==> Reading plugin metadata...' +data=($(python3 -c 'import sys,json;o=json.load(open("mcdreforged.plugin.json","r"));n=o["name"];d=o["id"];v=o["version"];an=o.get("archive_name");print(((n and n.replace(" ", ""))or d)+"-v"+v if not an else an.format(id=d,version=v));print(v)')) +if [ $? -ne 0 ]; then + echo '[ERROR] Cannot parse "mcdreforged.plugin.json"' + exit 1 +fi +name="${data[0]}" +version="v${data[1]}" + +echo '==> Packing source files...' +python3 -m mcdreforged pack -o ./output -n "$name" || exit $? + +if [ -n "$COMMIT" ]; then + +echo '==> Commiting git repo...' +( git add . && git commit -m "$version" && git push ) || exit $? + +if [ -n "$RELEASE" ]; then + +echo '==> Creating github release...' +gh release create "$version" "./output/${name}.mcdr" -t "$version" -n '' || exit $? + +fi +fi + +echo '==> Done' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/smart_backup/__init__.py b/smart_backup/__init__.py new file mode 100644 index 0000000..7a86c31 --- /dev/null +++ b/smart_backup/__init__.py @@ -0,0 +1,20 @@ + +import mcdreforged.api.all as MCDR +from .utils import * +from . import globals as GL +from . import commands as CMD + +def on_load(server: MCDR.PluginServerInterface, prev_module): + if prev_module is None: + log_info('Smart backup is on LOAD') + else: + log_info('Smart backup is on RELOAD') + GL.init(server) + CMD.register(server) + +def on_unload(server: MCDR.PluginServerInterface): + log_info('Smart backup is on UNLOAD') + GL.destory() + +def on_info(server: MCDR.ServerInterface, info: MCDR.Info): + CMD.on_info(server, info) diff --git a/smart_backup/commands.py b/smart_backup/commands.py new file mode 100644 index 0000000..3c8081d --- /dev/null +++ b/smart_backup/commands.py @@ -0,0 +1,141 @@ + +import mcdreforged.api.all as MCDR +from .utils import * +from . import globals as GL +from .objects import * + +Prefix = '!!smb' + +HelpMessage = ''' +{0} help 显示帮助信息 +{0} list [] 列出[所有/条]备份 +{0} query 查询备份详细信息 +{0} make [] 创建新备份(差异/全盘) +{0} makefull [] 创建全盘备份 +{0} rm [force] 删除指定备份(及其子备份) +{0} restore [] 回档至[上次/指定id]备份 +{0} confirm 确认操作 +{0} abort 取消操作 +{0} reload 重新加载配置文件 +{0} save 保存配置文件 +'''.strip().format(Prefix) + +game_saved_callback = None + +def on_info(server: MCDR.ServerInterface, info: MCDR.Info): + if not info.is_user: + global game_saved_callback + if game_saved_callback is not None and GL.Config.test_backup_trigger(info.content): + c, game_saved_callback = game_saved_callback, None + c() + +def register(server: MCDR.PluginServerInterface): + server.register_command( + MCDR.Literal(Prefix). + runs(command_help). + 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(GL.Config.literal('query'). + 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('confirm').runs(command_confirm)). + then(GL.Config.literal('abort').runs(command_abort)). + then(GL.Config.literal('reload').runs(command_config_load)). + then(GL.Config.literal('save').runs(command_config_save)) + ) + +def command_help(source: MCDR.CommandSource): + send_block_message(source, HelpMessage) + +def command_list_backup(source: MCDR.CommandSource, limit: int): + send_message(source, 'TODO: list "{}" backup'.format(limit)) + +@new_thread +def command_query_backup(source: MCDR.CommandSource, bid: str): + send_message(source, 'TODO: query backup "{}"'.format(bid)) + +@new_thread +@new_job('make backup') +def command_make(source: MCDR.CommandSource, comment: str): + server = source.get_server() + send_message(source, 'Making backup "{}"'.format(comment), log=True) + tuple(map(server.execute, GL.Config.befor_backup)) + + def call(): + mode = 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 + backup = Backup.create(mode, comment, + source.get_server().get_mcdr_config()['working_directory'], GL.Config.backup_needs, GL.Config.backup_ignores) + tuple(map(server.execute, GL.Config.after_backup)) + send_message(source, 'Saving backup "{}"'.format(comment), log=True) + backup.save(GL.Config.backup_path) + send_message(source, 'Saved backup "{}"'.format(comment), log=True) + + if len(GL.Config.start_backup_trigger_info) > 0: + begin_job() + global game_saved_callback + game_saved_callback = new_thread(lambda: (call(), after_job())) + else: + call() + +@new_thread +@new_job('make full backup') +def command_makefull(source: MCDR.CommandSource, comment: str): + server = source.get_server() + + def call(): + backup = Backup.create(BackupMode.FULL, comment, + source.get_server().get_mcdr_config()['working_directory'], GL.Config.backup_needs, GL.Config.backup_ignores) + tuple(map(server.execute, GL.Config.after_backup)) + send_message(source, 'Saving backup "{}"'.format(comment), log=True) + backup.save(GL.Config.backup_path) + send_message(source, 'Saved backup "{}"'.format(comment), log=True) + + begin_job() + global game_saved_callback + game_saved_callback = new_thread(lambda: (call(), after_job())) + + send_message(source, 'Making full backup "{}"'.format(comment), log=True) + tuple(map(server.execute, GL.Config.befor_backup)) + + +def command_confirm(source: MCDR.CommandSource): + confirm_map.pop(source.player if source.is_player else '', (lambda s: send_message(s, '当前没有正在执行的操作'), 0))[0](source) + +def command_abort(source: MCDR.CommandSource): + c = confirm_map.pop(source.player if source.is_player else '', (0, 0))[1] + if not c: + c = confirm_map.pop(None, (0, lambda s: send_message(s, '当前没有正在执行的操作')))[1] + c(source) + +@new_thread +def command_config_load(source: MCDR.CommandSource): + GL.Config = server.load_config_simple(target_class=GL.SMBConfig, source_to_reply=source) + +@new_thread +def command_config_save(source: MCDR.CommandSource): + GL.Config.save() + send_message(source, 'Save config file SUCCESS') + +confirm_map = {} + +def __warp_call(call): + def c(*b): + return call(*b[:call.__code__.co_argcount]) + return c + +def register_confirm(player: str, confirm_call, abort_call=lambda: 0): + confirm_map[player] = (__warp_call(confirm_call), __warp_call(abort_call)) diff --git a/smart_backup/globals.py b/smart_backup/globals.py new file mode 100644 index 0000000..9d319fe --- /dev/null +++ b/smart_backup/globals.py @@ -0,0 +1,78 @@ + +import re +from typing import List, Dict, Any + +import mcdreforged.api.all as MCDR + +__all__ = [ + 'MSG_ID', 'BIG_BLOCK_BEFOR', 'BIG_BLOCK_AFTER', 'SMBConfig', 'Config', 'SERVER_INS', 'init', 'destory' +] + +MSG_ID = MCDR.RText('[SMB]', color=MCDR.RColor.green) +BIG_BLOCK_BEFOR = '------------ {0} v{1} ::::' +BIG_BLOCK_AFTER = ':::: {0} v{1} ============' + +class SMBConfig(MCDR.Serializable): + differential_backup_limit: int = 10 + full_backup_limit: int = 10 + backup_interval: int = 60 * 60 * 1 # 1 hours + last_backup_time: int = 0 + restore_timeout: int = 30 + backup_path: str = './smt_backups' + overwrite_path: str = './smt_backup_overwrite' + backup_needs: List[str] = ['world'] + backup_ignores: List[str] = ['session.lock'] + befor_backup: List[str] = ['save-off', 'save-all flush'] + start_backup_trigger_info: str = r'Saved the (?:game|world)' + after_backup: List[str] = ['save-on'] + # 0:guest 1:user 2:helper 3:admin 4:owner + minimum_permission_level: Dict[str, int] = { + 'help': 0, + 'status': 1, + 'list': 1, + 'make': 2, + 'makefull': 3, + 'back': 3, + 'confirm': 1, + 'abort': 1, + 'reload': 3, + 'save': 3, + } + _cache: Dict[str, Any] = {} + + def test_backup_trigger(self, info: str): + if not hasattr(self, '__start_backup_trigger') or self.__start_backup_trigger_info != self.start_backup_trigger_info: + self.__start_backup_trigger_info = self.start_backup_trigger_info + self.__start_backup_trigger = re.compile(self.start_backup_trigger_info) + return self.__start_backup_trigger.fullmatch(info) is not None + + @property + def cache(self): + return self._cache + + def literal(self, literal: str): + lvl = self.minimum_permission_level.get(literal, 0) + return MCDR.Literal(literal).requires(lambda src: src.has_permission(lvl), + lambda: MCDR.RText(MSG_ID.to_plain_text() + ' 权限不足', color=MCDR.RColor.red)) + + def save(self): + SERVER_INS.save_config_simple(self) + + +Config: SMBConfig = SMBConfig() +SERVER_INS: MCDR.PluginServerInterface = None + +def init(server: MCDR.PluginServerInterface): + global SERVER_INS + SERVER_INS = server + global BIG_BLOCK_BEFOR, BIG_BLOCK_AFTER + metadata = server.get_self_metadata() + BIG_BLOCK_BEFOR = BIG_BLOCK_BEFOR.format(metadata.name, metadata.version) + BIG_BLOCK_AFTER = BIG_BLOCK_AFTER.format(metadata.name, metadata.version) + global Config + Config = server.load_config_simple(target_class=SMBConfig) + +def destory(): + global SERVER_INS + Config.save() + SERVER_INS = None diff --git a/smart_backup/objects.py b/smart_backup/objects.py new file mode 100644 index 0000000..7637353 --- /dev/null +++ b/smart_backup/objects.py @@ -0,0 +1,418 @@ + +import os +import time +import enum +import weakref +import queue + +__all__ = [ + 'ModifiedType', 'BackupMode', 'BackupFile', 'BackupDir', 'Backup' +] + +class ModifiedType(int, enum.Enum): + UNKNOWN = 0 + UPDATE = 1 + REMOVE = 2 + +class BackupMode(int, enum.Enum): + FULL = 0 + INCREMENTAL = 1 + DIFFERENTIAL = 2 + + +class BackupFile: pass +class BackupDir: pass +class Backup: pass + +class BackupFile: + def __init__(self, type_: ModifiedType, name: str, mode: int, data: bytes = None, path: str = None, offset: int = -1): + self._type = type_ + self._name = name + self._mode = mode + self._data = data + self._path = path + self._offset = offset + + @property + def type(self): + return self._type + + @property + def name(self): + return self._name + + @property + def mode(self): + return self._mode + + @property + def data(self): + if self._type == ModifiedType.REMOVE: + return None + if self._data is not None: + return self._data + if self._path is not None: + with open(self._path, 'rb') as fd: + fd.seek(self._offset) + return fd.read() + + def get(self, base, *path): + return None + + @classmethod + def create(cls, path: str, *pt, filterc=None, prev: Backup = None): + type_: ModifiedType + name: str = pt[-1] + mode: int + if os.path.exists(path): + mode = os.stat(path).st_mode & 0o777 + with open(path, 'rb') as fd: + data = fd.read() + if prev is not None: + pref = prev.get(*pt) + if isinstance(pref, cls) and mode == pref.mode and data == pref.data: + return None + type_ = ModifiedType.UPDATE + else: + type_ = ModifiedType.REMOVE + mode = 0 + return cls(type_=type_, name=name, mode=mode, path=path, offset=0) + + def restore(self, path: str): + with open(path, 'wb') as wd: + if self._data is not None: + wd.write(self._data) + else: + with open(self._path, 'rb') as rd: + rd.seek(self._offset) + while True: + b = rd.read(8192) + if not b: + break + wd.write(b) + + def save(self, path: str): + path = os.path.join(path, self._name + '.F') + with open(path, 'wb') as fd: + fd.write(self._type.to_bytes(1, byteorder='big')) + if self._type != ModifiedType.REMOVE: + fd.write(self._mode.to_bytes(2, byteorder='big')) + data = self.data + # TODO compress data + fd.write(data) + self._path, self._offset = path, 3 + + @classmethod + def load(cls, path: str, prev: BackupFile = None): + type_: ModifiedType + name: str = os.path.splitext(os.path.basename(path))[0] + mode: int + with open(path, 'rb') as fd: + type_ = ModifiedType(int.from_bytes(fd.read(1), byteorder='big')) + if type_ == ModifiedType.REMOVE: + mode = 0 + else: + mode = int.from_bytes(fd.read(2), byteorder='big') + return cls(type_=type_, name=name, mode=mode, path=path, offset=3) + +class BackupDir: + def __init__(self, type_: ModifiedType, name: str, mode: int, files: dict = None): + self._type = type_ + self._name = name + self._mode = mode + self._files = dict((f.name, f) for f in files) if isinstance(files, (list, tuple, set)) else files.copy() if isinstance(files, dict) else {} + + @property + def type(self): + return self._type + + @property + def name(self): + return self._name + + @property + def mode(self): + return self._mode + + @property + def files(self): + return self._files.copy() + + def get(self, base, *path): + f = self._files.get(base, None) + if f is not None and len(path) > 0: + f = f.get(*path) + return f + + @classmethod + def create(cls, path: str, *pt, filterc=lambda _: True, prev: Backup = None): + type_: ModifiedType + name: str = pt[-1] + mode: int + files: list = [] + if os.path.isdir(path): + mode = os.stat(path).st_mode & 0o777 + l = set(os.listdir(path)) + if prev is not None: + l.update(prev.get_total_files(*pt)) + for n in filter(filterc, l): + f = os.path.join(path, n) + o = (BackupDir if os.path.isdir(f) else BackupFile if os.path.exists(f) else prev.get(*pt, n).__class__).create(f, *pt, n, prev=prev) + if o is not None: + files.append(o) + if prev is not None and len(files) == 0 and pt[-1] in prev.get_total_files(*pt[:-1]): + return None + type_ = ModifiedType.UPDATE + else: + type_ = ModifiedType.REMOVE + mode = 0 + return cls(type_=type_, name=name, mode=mode, files=files) + + def save(self, path: str): + path = os.path.join(path, self._name + '.D') + if self._type == ModifiedType.REMOVE: + with open(path, 'wb') as fd: + fd.write(self._type.to_bytes(1, byteorder='big')) + else: + os.mkdir(path) + with open(os.path.join(path, '0'), 'wb') as fd: + fd.write( + self._type.to_bytes(1, byteorder='big') + + self._mode.to_bytes(2, byteorder='big')) + for f in self._files.values(): + f.save(path) + + @classmethod + def load(cls, path: str): + type_: ModifiedType + name: str = os.path.splitext(os.path.basename(path))[0] + mode: int + files: list = [] + if os.path.isdir(path): + with open(os.path.join(path, '0'), 'rb') as fd: + type_ = ModifiedType(int.from_bytes(fd.read(1), byteorder='big')) + if type_ != ModifiedType.REMOVE: + mode = int.from_bytes(fd.read(2), byteorder='big') + if type_ != ModifiedType.REMOVE: + for n in os.listdir(path): + f = os.path.join(path, n) + e = os.path.splitext(f)[1] + if e == '.F': + files.append(BackupFile.load(f)) + elif e == '.D': + files.append(BackupDir.load(f)) + else: + with open(path, 'rb') as fd: + type_ = ModifiedType(int.from_bytes(fd.read(1), byteorder='big')) + assert type_ == ModifiedType.REMOVE + mode = 0 + return cls(type_=type_, name=name, mode=mode, files=files) + +class Backup: + _cache = weakref.WeakValueDictionary() + + def __init__(self, mode: BackupMode, timestamp: int, comment: str, files: dict = None, prev: Backup = None): + self._mode = mode + self._timestamp = timestamp # unit: ms + self._comment = comment + self._files = dict((f.name, f) for f in files) if isinstance(files, (list, tuple, set)) else files.copy() if isinstance(files, dict) else {} + self._prev = prev + + @property + def mode(self): + return self._mode + + @property + def timestamp(self): + return self._timestamp + + @property + def comment(self): + return self._comment + + @property + def prev(self): + return self._prev + + @property + def files(self): + return self._files.copy() + + def get_total_files(self, *path): + files = self + for f in path: + files = files.files.get(f) + if files is None: + filel = set() + break + else: + filel = set(files.files.keys()) + if self._mode != BackupMode.FULL: + filel.update(self._prev.get_total_files(*path)) + return set(filter(lambda a: self.get(*path, a).type != ModifiedType.REMOVE, filel)) + + def get(self, base, *path): + f = self._files.get(base, None) + if f is not None and len(path) > 0: + f = f.get(*path) + if f is None and self._prev is not None: + return self._prev.get(base, *path) + return f + + @classmethod + def create(cls, mode: BackupMode, comment: str, base: str, needs: list, ignores: list = []): + timestamp: int = int(time.time() * 1000) + files: list = [] + prev: Backup = None + l = set(os.listdir(base)) + if mode != BackupMode.FULL: + prev = cls.get_last() + assert prev is not None + if mode == BackupMode.DIFFERENTIAL: + while prev.mode != BackupMode.FULL: + prev = prev.prev + l.update(prev.get_total_files()) + filterc = filters(ignores) + for n in filter(lambda a: a in needs, l): + m = os.path.join(base, n) + o = (BackupDir if os.path.isdir(m) else BackupFile if os.path.exists(m) else prev.get(n).__class__).create(m, n, filterc=filterc, prev=prev) + if o is not None: + files.append(o) + return cls(mode=mode, timestamp=timestamp, comment=comment, files=files, prev=prev) + + def restore(self, path: str, needs: list, ignores: list = []): + files = [] + que = queue.SimpleQueue() + for f in self.get_total_files(): + que.put([f]) + while not que.empty(): + n = que.get_nowait() + f = self.get(*n) + files.append([os.path.join(path, *n), f]) + if isinstance(f, BackupDir): + for m in self.get_total_files(*n): + que.put([*n, m]) + filterc = filters(ignores) + for n in needs: + clear_dir(os.path.join(path, n), filterc) + for p, f in files: + if isinstance(f, BackupDir) and not os.path.exists(p): + os.makedirs(p) + elif isinstance(f, BackupFile): + f.restore(p) + + def save(self, path: str): + if not os.path.exists(path): + os.makedirs(path) + path = os.path.join(path, hex(self._timestamp)) + os.mkdir(path) + with open(os.path.join(path, '0'), 'wb') as fd: + comment = self._comment.encode('utf8') + fd.write( + self._mode.to_bytes(1, byteorder='big') + + (0 if self._prev is None else self._prev.timestamp).to_bytes(8, byteorder='big') + + len(comment).to_bytes(2, byteorder='big')) + fd.write(comment) + for f in self._files.values(): + f.save(path) + + @classmethod + def load(cls, path: str): + rpath = os.path.realpath(path) + if rpath in cls._cache: + return cls._cache[rpath] + mode: BackupMode + timestamp: int = int(os.path.basename(path), 16) + comment: str + files: list = [] + prev: Backup = None + with open(os.path.join(path, '0'), 'rb') as fd: + mode = BackupMode(int.from_bytes(fd.read(1), byteorder='big')) + previd = int.from_bytes(fd.read(8), byteorder='big') + if previd != 0: + assert previd != timestamp + prev = cls.load(os.path.join(os.path.dirname(path), hex(previd))) + comment = fd.read(int.from_bytes(fd.read(2), byteorder='big')).decode('utf8') + for n in os.listdir(path): + f = os.path.join(path, n) + e = os.path.splitext(f)[1] + if e == '.F': + files.append(BackupFile.load(f)) + elif e == '.D': + files.append(BackupDir.load(f)) + obj = cls(mode=mode, timestamp=timestamp, comment=comment, files=files, prev=prev) + cls._cache[rpath] = obj + return obj + + def __hash__(self): + return hash(hex(self.timestamp)) + + @staticmethod + def list(path: str, limit: int = -1): + 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): + ids = sorted(map(lambda a: int(a, 16), filter(lambda a: a.startswith('0x'), os.listdir(path)))) + if len(ids) == 0: + return None + return Backup.load(os.path.join(path, hex(ids[-1]))) + + +def _filter(ignore: str): + if len(ignore) == 0: + return lambda *a, **b: 1 + if ignore[0] == '/': + ignore = ignore[1:] + def call(path: str): + return 1 if path == ignore else 0 + else: + def call(path: str): + return 1 if os.path.basename(path) == ignore else 0 + return call + +def filters(ignores: list): + for i, s in enumerate(ignores): + ignores[i] = _filter(s) + + def call(path: str): + for c in ignores: + s = c(path) + if s == 1: + return False + return True + return call + +def clear_dir(path: str, filterc): + if not os.path.exists(path): + return + if not os.path.isdir(path): + os.remove(path) + return + que = queue.SimpleQueue() + dirs = {} + for f in os.listdir(path): + que.put(os.path.join(path, f)) + while not que.empty(): + f = que.get_nowait() + if not filterc(os.path.basename(f)): + continue + if os.path.isdir(f): + ls = os.listdir(f) + if len(ls) == 0: + os.rmdir(f) + else: + dirs[f] = len(ls) + for i in ls: + que.put(os.path.join(f, i)) + else: + os.remove(f) + d = os.path.dirname(f) + if d in dirs: + dirs[d] -= 1 + if dirs[d] == 0: + dirs.pop(d) + os.rmdir(d) diff --git a/smart_backup/utils.py b/smart_backup/utils.py new file mode 100644 index 0000000..c4bf4df --- /dev/null +++ b/smart_backup/utils.py @@ -0,0 +1,97 @@ + +from threading import RLock, Condition +import functools + +import mcdreforged.api.all as MCDR +from . import globals as GL + +__all__ = [ + 'new_thread', 'get_current_job', 'begin_job', 'after_job', 'new_job', + 'join_rtext', 'send_block_message', 'send_message', 'broadcast_message', 'log_info' +] + +def new_thread(call): + @MCDR.new_thread('smart_backup') + def c(*args, **kwargs): + return call(*args, **kwargs) + return c + +current_job = None +job_lock = Condition(RLock()) + +def get_current_job(): + with job_lock: + return None if current_job is None else current_job[0] + +def check_job(job: str): + with job_lock: + return current_job is None or current_job[0] == job + +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: + 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: + current_job[1] += 1 + return True + if not block: + break + job_lock.wait() + return False + +def after_job(): + global current_job, job_lock + with job_lock: + assert current_job is not None + current_job[1] -= 1 + if current_job[1] == 0: + current_job = None + job_lock.notify() + +def new_job(job: str): + 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): + send_message(args[0], MCDR.RText('In progress {} now'.format(current_job[0]), color=MCDR.RColor.red)) + return None + else: + begin_job(job, block=True) + try: + return call(*args, **kwargs) + finally: + after_job() + return c + return w + +def join_rtext(*args, sep=' '): + return MCDR.RTextList(args[0], *(MCDR.RTextList(sep, a) for a in args[1:])) + +def send_block_message(source: MCDR.CommandSource, *args, sep='\n', log=False): + if source is not None: + t = join_rtext(GL.BIG_BLOCK_BEFOR, join_rtext(*args, sep=sep), GL.BIG_BLOCK_AFTER, sep='\n') + source.reply(t) + if log and not source.is_console: + source.get_server().logger.info(t) + + +def send_message(source: MCDR.CommandSource, *args, sep=' ', prefix=GL.MSG_ID, log=False): + if source is not None: + t = join_rtext(prefix, *args, sep=sep) + source.reply(t) + if log and not source.is_console: + source.get_server().logger.info(t) + +def broadcast_message(*args, sep=' ', prefix=GL.MSG_ID): + if GL.SERVER_INS is not None: + GL.SERVER_INS.broadcast(join_rtext(prefix, *args, sep=sep)) + +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))