diff --git a/lang/en_us.yml b/lang/en_us.yml index 73221aa..6e0bad7 100644 --- a/lang/en_us.yml +++ b/lang/en_us.yml @@ -183,13 +183,13 @@ prime_backup: §7{prefix}§r: Display the current welcome page §7{prefix} help §e[]§r: Display help message of all / given command §7{prefix} make §e[]§r: Make a backup. §e§r is an optional comment message - §7{prefix} back §6[]§r: Restore the world to the given backup. Default: latest backup + §7{prefix} back §6[]§r: Restore to the given backup. See §7{prefix} help back§r for detailed help §7{prefix} list [...]§r: List backups with given filters. See §7{prefix} help list§r for detailed help §7{prefix} show §6§r: Show detailed information of the given backup §7{prefix} rename §6§r §e§r: Modify the comment of the given backup - §7{prefix} delete §6§r: Delete the given backup + §7{prefix} delete §6 [...]§r: Delete the given backup. You can enter multiple backup IDs §7{prefix} delete_range §6§r: Delete backups inside the given ID range - §7{prefix} export §6 §3[]§r: Export the given backup to the §3export§r folder + §7{prefix} export §6 §7[...]§r: Export the given backup. See §7{prefix} help export§r for detailed help §7{prefix} prune §6§r: Manually trigger a backup prune §7{prefix} crontab §5 §7[...]§r: Crontab job operations. See §7{prefix} help crontab§r for help §7{prefix} tag §6 §7[...]§r: Tag operation on the given backup @@ -199,15 +199,24 @@ prime_backup: arguments: title: '[Arguments]' content: |- - §6§r: A positive integer, the unique identifier for a backup, e.g.: §612§r + §6§r: A positive integer, the unique ID for a backup, e.g.: §612§r §6§r: An integer closed interval, e.g.: §63-12§r, §64~9§r, §64~§r, §64~§r, §6*§r - §3§r: Available options: {export_formats} other: title: '[Others]' nodes_with_help: 'Subcommands with detailed help: {}' docs: 'Documentation: {}' docs.hover: Click to open the url node_help: + back: |- + §d[back Command Usage]§r + Restore the world to the given backup + §7{prefix} back §6[] §7[--flags]§r + §d[Arguments]§r + §3§r: The ID of the backup to restore. If not specified, the latest not pre-restore backup will be used + §d[Optional flags]§r + §7--confirm§r: Skip the confirm step and start the restore directly + §7--fail-soft§r: Skip files with export failure in the backup, so a single failure will not abort the export + §7--no-verify§r: Do not verify the exported file contents crontab: |- §d[crontab Command Usage]§r Operate crontab jobs @@ -233,6 +242,20 @@ prime_backup: - §aall§r: Validate all of the above database.scheduled_compact.on: 'Notes: With the current config, {name} will automatically prune the database every {interval}' database.scheduled_compact.off: 'Notes: With the current config, scheduled database prune is disabled' + export: |- + §d[export Command Usage]§r + Export the given backup to the §3export§r folder + §7{prefix} export §6 §3[] §7[--flags]§r + §d[Arguments]§r + §3§r: Available options: {export_formats}. Use §3tar§r format if not specified + §d[Optional flags]§r + §7--overwrite§r: Overwrites existing exported backup. By default, no export will be made if the output file exists + §7--fail-soft§r: Skip files with export failure in the backup, so a single failure will not abort the export + §7--no-verify§r: Do not verify the exported file contents + §d[Examples]§r + §7{prefix} export 12§r: Use the default §3tar§r format to export backup §612§r + §7{prefix} export 12 tar_gz§r: Use the §tar_gz§r format to export backup §612§r + §7{prefix} export 12 tar --fail-soft --no-verify§r: Export backup §612§r with best effort list: |- §d[list Command Usage]§r List backups with given filters @@ -242,11 +265,11 @@ prime_backup: §7--author §e§r: Show backup created by the given author only §7--start §b§r: Show backup after the given date only §7--end §b§r: Show backup before the given date only + §7--all§r: Show all backups. By default, hidden backups and pre-restore backups will not be shown §7--size§r: Show backup sizes §7--flags§r: Show backup flags, based on the tags of the backup - §7--all§r: Show all backups. By default, hidden backups and pre-restore backups will not be shown §d[Examples]§r - §7{prefix} list --all§r + §7{prefix} list --all --flags§r §7{prefix} list --start 20231130 --size§r §7{prefix} list 3 --per-page 20§r tag: |- diff --git a/lang/zh_cn.yml b/lang/zh_cn.yml index fe8a9d1..ab50069 100644 --- a/lang/zh_cn.yml +++ b/lang/zh_cn.yml @@ -183,13 +183,13 @@ prime_backup: §7{prefix}§r: 展示当前这个欢迎界面 §7{prefix} help §e[<指令>]§r: 展示全部指令/给定指令的详细帮助 §7{prefix} make §e[<注释>]§r: 创建一个备份。§e<注释>§r为可选注释 - §7{prefix} back §6[<备份ID>]§r: 回档至给定备份。若未指定备份ID, 则使用最新的备份 + §7{prefix} back §6[<备份ID>]§r: 回档至给定备份。详见§7{prefix} help back§r §7{prefix} list [...]§r: 列出备份, 展示备份列表。详见§7{prefix} help list§r §7{prefix} show §6<备份ID>§r: 展示给定备份的详细信息 §7{prefix} rename §6<备份ID>§r §e<新注释>§r: 修改给定备份的注释 - §7{prefix} delete §6<备份ID>§r: 删除给定备份 - §7{prefix} delete_range §6<备份ID范围>§r: 删除给定ID范围的备份。参数例子: §62-7§r、§69-§r、§6*§r - §7{prefix} export §6<备份ID> §3[<导出格式>]§r: 以给定格式导出给定备份到§3export§r文件夹 + §7{prefix} delete §6<备份ID> [<备份ID>...]§r: 删除给定备份。可输入多个备份ID + §7{prefix} delete_range §6<备份ID范围>§r: 删除给定ID范围的备份 + §7{prefix} export §6<备份ID> §7[...]§r: 给导出给定备份到文件。详见§7{prefix} help export§r §7{prefix} prune §6<备份ID>§r: 手动触发一次备份裁剪 §7{prefix} crontab §5<任务ID> §7[...]§r: 操作定时任务。详见§7{prefix} help crontab§r §7{prefix} tag §6<备份ID> §7[...]§r: 操作给定备份的标签。详见§7{prefix} help tag§r @@ -199,15 +199,24 @@ prime_backup: arguments: title: 【参数帮助】 content: |- - §6<备份ID>§r: 一个正整数, 备份的唯一标识符, 如: §612§r + §6<备份ID>§r: 一个正整数, 备份的唯一ID, 如: §612§r §6<备份ID范围>§r: 一个整数闭区间, 如: §63-12§r, §64~9§r, §64~§r, §64~§r, §6*§r - §3<导出格式>§r: 可用选项: {export_formats} other: title: 【其它】 nodes_with_help: '含详细帮助页的子命令: {}' docs: '文档: {}' docs.hover: 点击以打开链接 node_help: + back: |- + §d【back指令帮助】§r + §7{prefix} back §6[<备份ID>] §7[--可选参数]§r + 回档至给定备份 + §d【参数帮助】§r + §6<备份ID>§r: 备份的ID。若未给出,则使用非回档前自动备份的最新的备份 + §d【可选参数】§r + §7--confirm§r: 跳过确认步骤,直接开始回档 + §7--fail-soft§r: 在导出过程中跳过导出失败的文件,因此单个文件的失败不会导致整个导出的失败 + §7--no-verify§r: 不校验导出文件的内容 crontab: |- §d【crontab指令帮助】§r 操作定时任务 @@ -233,6 +242,20 @@ prime_backup: - §aall§r: 验证上述全部 database.scheduled_compact.on: '注意: 在当前配置下,{name}每{interval}会自动执行一次数据库精简操作' database.scheduled_compact.off: '注意: 在当前配置下,定时数据库精简操作已被禁用' + export: |- + §d【export指令帮助】§r + §7{prefix} export §6<备份ID> §3[<导出格式>] §7[--可选参数]§r + 以给定格式导出给定备份到§3export§r文件夹 + §d【参数帮助】§r + §3<导出格式>§r: 可用选项: {export_formats}。若未指定,则使用§3tar§r格式 + §d【可选参数】§r + §7--overwrite§r: 覆盖已存在的备份导出文件。默认情况下,若输出文件已存在则不导出 + §7--fail-soft§r: 在导出过程中跳过导出失败的文件,因此单个文件的失败不会导致整个导出的失败 + §7--no-verify§r: 不校验导出文件的内容 + §d【例子】§r + §7{prefix} export 12§r: 使用默认的§3tar§r格式导出备份§612§r + §7{prefix} export 12 tar_gz§r: 使用§3tar_gz§r格式导出备份§612§r + §7{prefix} export 12 tar --fail-soft --no-verify§r: 使用§3tar§r格式尽力而为地导出备份§612§r list: |- §d【list指令帮助】§r 列出备份, 展示备份列表 @@ -242,11 +265,11 @@ prime_backup: §7--author §e<作者>§r: 仅列出给定作者的备份 §7--start §b<起始日期>§r: 仅列出给定日期之后的备份 §7--end §b<结束日期>§r: 仅列出给定日期之前的备份 + §7--all§r: 展示所有的备份。默认情况下, 隐藏备份、回档前备份不会被展示 §7--size§r: 展示备份原始大小。这将增加任务耗时 §7--flags§r: 展示备份标志位, 这些标志位是基于备份的标签生成的 - §7--all§r: 展示所有的备份。默认情况下, 隐藏备份、回档前备份不会被展示 §d【例子】§r - §7{prefix} list --all§r + §7{prefix} list --all --flags§r §7{prefix} list --start 20231130 --size§r §7{prefix} list 3 --per-page 20§r tag: |- diff --git a/prime_backup/action/export_backup_action.py b/prime_backup/action/export_backup_action.py index 156a49d..392d52f 100644 --- a/prime_backup/action/export_backup_action.py +++ b/prime_backup/action/export_backup_action.py @@ -77,10 +77,11 @@ def _i_am_root(): class ExportBackupToDirectoryAction(_ExportBackupActionBase): def __init__( self, backup_id: int, output_path: Path, *, - fail_soft: bool = False, delete_existing: bool = True, + fail_soft: bool = False, verify_blob: bool = True, + delete_existing: bool = True, child_to_export: Optional[Path] = None, recursively_export_child: bool = False, ): - super().__init__(backup_id, output_path, fail_soft=fail_soft) + super().__init__(backup_id, output_path, fail_soft=fail_soft, verify_blob=verify_blob) self.delete_existing = delete_existing self.child_to_export = child_to_export self.recursively_export_child = recursively_export_child @@ -192,8 +193,11 @@ def _export_backup(self, session, backup: schema.Backup) -> ExportFailures: class ExportBackupToTarAction(_ExportBackupActionBase): - def __init__(self, backup_id: int, output_path: Path, tar_format: TarFormat, *, fail_soft: bool = False): - super().__init__(backup_id, output_path, fail_soft=fail_soft) + def __init__( + self, backup_id: int, output_path: Path, tar_format: TarFormat, *, + fail_soft: bool = False, verify_blob: bool = True, + ): + super().__init__(backup_id, output_path, fail_soft=fail_soft, verify_blob=verify_blob) self.tar_format = tar_format @contextlib.contextmanager @@ -228,6 +232,7 @@ def __export_file(self, tar: tarfile.TarFile, file: schema.File): reader = None tar.addfile(tarinfo=info, fileobj=stream) if reader is not None: + # notes: the read len is always <= info.size self._verify_exported_blob(file, reader.get_read_len(), reader.get_hash()) elif stat.S_ISDIR(file.mode): diff --git a/prime_backup/cli/entrypoint.py b/prime_backup/cli/entrypoint.py index c247fdf..cafbd92 100644 --- a/prime_backup/cli/entrypoint.py +++ b/prime_backup/cli/entrypoint.py @@ -133,10 +133,11 @@ def cmd_export(self): backup = GetBackupAction(self.args.backup_id).run() logger.info('Exporting backup #{} to {}, format {}'.format(backup.id, str(output_path.as_posix()), fmt.name)) + kwargs = dict(fail_soft=self.args.fail_soft, verify_blob=not self.args.no_verify) if isinstance(fmt.value, TarFormat): - act = ExportBackupToTarAction(backup.id, output_path, fmt.value) + act = ExportBackupToTarAction(backup.id, output_path, fmt.value, **kwargs) else: - act = ExportBackupToZipAction(backup.id, output_path) + act = ExportBackupToZipAction(backup.id, output_path, **kwargs) failures = act.run() if len(failures) > 0: @@ -204,6 +205,8 @@ def entrypoint(cls): parser_export.add_argument('backup_id', type=int, help='The ID of the backup to export') parser_export.add_argument('output', help='The output file name of the exported backup. Example: my_backup.tar') parser_export.add_argument('-f', '--format', help='The format of the output file. If not given, attempt to infer from the output file name. Options: {}'.format(enum_options(StandaloneBackupFormat))) + parser_export.add_argument('--fail-soft', action='store_true', help='Skip files with export failure in the backup, so a single failure will not abort the export') + parser_export.add_argument('--no-verify', action='store_true', help='Do not verify the exported file contents') desc = 'Extract a single file from a backup' parser_extract = subparsers.add_parser('extract', help=desc, description=desc) diff --git a/prime_backup/mcdr/command/commands.py b/prime_backup/mcdr/command/commands.py index b8a9c1f..c83eb89 100644 --- a/prime_backup/mcdr/command/commands.py +++ b/prime_backup/mcdr/command/commands.py @@ -80,8 +80,9 @@ def callback(_, err): def cmd_back(self, source: CommandSource, context: CommandContext): needs_confirm = context.get('confirm', 0) == 0 fail_soft = context.get('fail_soft', 0) > 0 + verify_blob = context.get('no_verify', 0) == 0 backup_id = context.get('backup_id') - self.task_manager.add_task(RestoreBackupTask(source, backup_id, needs_confirm=needs_confirm, fail_soft=fail_soft)) + self.task_manager.add_task(RestoreBackupTask(source, backup_id, needs_confirm=needs_confirm, fail_soft=fail_soft, verify_blob=verify_blob)) def cmd_list(self, source: CommandSource, context: CommandContext): page = context.get('page', 1) @@ -129,7 +130,9 @@ def cmd_export(self, source: CommandSource, context: CommandContext): backup_id = context['backup_id'] export_format = context.get('export_format', StandaloneBackupFormat.tar) fail_soft = context.get('fail_soft', 0) > 0 - self.task_manager.add_task(ExportBackupTask(source, backup_id, export_format, fail_soft=fail_soft)) + verify_blob = context.get('no_verify', 0) == 0 + overwrite_existing = context.get('overwrite', 0) > 0 + self.task_manager.add_task(ExportBackupTask(source, backup_id, export_format, fail_soft=fail_soft, verify_blob=verify_blob, overwrite_existing=overwrite_existing)) def cmd_crontab_show(self, source: CommandSource, context: CommandContext): job_id = context.get('job_id') @@ -255,6 +258,9 @@ def set_confirm_able(node: AbstractNode): def set_fail_soft_able(node: AbstractNode): node.then(CountingLiteral('--fail-soft', 'fail_soft').redirects(node)) + def set_no_verify_able(node: AbstractNode): + node.then(CountingLiteral('--no-verify', 'no_verify').redirects(node)) + def make_back_cmd() -> Literal: node_sc = create_subcommand('back') node_bid = create_backup_id() @@ -262,6 +268,7 @@ def make_back_cmd() -> Literal: for node in [node_sc, node_bid]: set_confirm_able(node) set_fail_soft_able(node) + set_no_verify_able(node) node.runs(self.cmd_back) return node_sc @@ -281,6 +288,8 @@ def make_export_cmd() -> Literal: for node in [node_bid, node_ef]: set_fail_soft_able(node) + set_no_verify_able(node) + node.then(CountingLiteral('--overwrite', 'overwrite').redirects(node)) node.runs(self.cmd_export) return node_sc diff --git a/prime_backup/mcdr/task/backup/export_backup_task.py b/prime_backup/mcdr/task/backup/export_backup_task.py index 7b3ea03..c0e9995 100644 --- a/prime_backup/mcdr/task/backup/export_backup_task.py +++ b/prime_backup/mcdr/task/backup/export_backup_task.py @@ -19,11 +19,16 @@ def _sanitize_file_name(s: str, max_length: int = 64): class ExportBackupTask(OperationTask[None]): - def __init__(self, source: CommandSource, backup_id: int, export_format: StandaloneBackupFormat, fail_soft: bool): + def __init__( + self, source: CommandSource, backup_id: int, export_format: StandaloneBackupFormat, *, + fail_soft: bool, verify_blob: bool, overwrite_existing: bool, + ): super().__init__(source) self.backup_id = backup_id self.export_format = export_format self.fail_soft = fail_soft + self.verify_blob = verify_blob + self.overwrite_existing = overwrite_existing @property def id(self) -> str: @@ -41,7 +46,7 @@ def make_output(extension: str) -> Path: return self.config.storage_path / 'export' / name efv = self.export_format.value - kwargs = dict(fail_soft=self.fail_soft) + kwargs = dict(fail_soft=self.fail_soft, verify_blob=self.verify_blob) if isinstance(efv, TarFormat): path = make_output(efv.value.extension) action = ExportBackupToTarAction(self.backup_id, path, efv, **kwargs) @@ -51,7 +56,7 @@ def make_output(extension: str) -> Path: else: raise TypeError(efv) - if path.exists(): + if path.exists() and not self.overwrite_existing: self.reply(self.tr('already_exists', TextComponents.file_path(path))) return diff --git a/prime_backup/mcdr/task/backup/restore_backup_task.py b/prime_backup/mcdr/task/backup/restore_backup_task.py index 719f7b5..9c41ea7 100644 --- a/prime_backup/mcdr/task/backup/restore_backup_task.py +++ b/prime_backup/mcdr/task/backup/restore_backup_task.py @@ -17,11 +17,12 @@ class RestoreBackupTask(OperationTask[None]): - def __init__(self, source: CommandSource, backup_id: Optional[int] = None, needs_confirm: bool = True, fail_soft: bool = False): + def __init__(self, source: CommandSource, backup_id: Optional[int] = None, needs_confirm: bool = True, fail_soft: bool = False, verify_blob: bool = True): super().__init__(source) self.backup_id = backup_id self.needs_confirm = needs_confirm self.fail_soft = fail_soft + self.verify_blob = verify_blob @property def id(self) -> str: @@ -83,8 +84,8 @@ def run(self): ).run() cost_backup = timer.get_and_restart() - self.logger.info('Restoring backup (fail-soft={})'.format(self.fail_soft)) - ExportBackupToDirectoryAction(backup.id, self.config.source_path, delete_existing=True, fail_soft=self.fail_soft).run() + self.logger.info('Restoring backup (fail_soft={}, verify_blob={})'.format(self.fail_soft, self.no_verify)) + ExportBackupToDirectoryAction(backup.id, self.config.source_path, delete_existing=True, fail_soft=self.fail_soft, verify_blob=self.verify_blob).run() cost_restore = timer.get_and_restart() self.logger.info('Restore done, cost {}s (backup {}s, restore {}s), starting the server'.format( diff --git a/prime_backup/mcdr/task/general/show_help_task.py b/prime_backup/mcdr/task/general/show_help_task.py index 7d483fd..885f532 100644 --- a/prime_backup/mcdr/task/general/show_help_task.py +++ b/prime_backup/mcdr/task/general/show_help_task.py @@ -12,8 +12,10 @@ class ShowHelpTask(ImmediateTask[None]): COMMANDS_WITH_DETAILED_HELP = [ + 'back', 'crontab', 'database', + 'export', 'list', 'tag', ] @@ -45,13 +47,10 @@ def __has_permission(self, literal: str) -> bool: def run(self) -> None: with self.source.preferred_language_context(): if self.what is None: - from prime_backup.types.standalone_backup_format import StandaloneBackupFormat - t_export_formats = ', '.join([f'§3{ebf.name}§r' for ebf in StandaloneBackupFormat]) - self.reply(self.tr('commands.title').set_color(TextColors.help_title)) self.__reply_help(self.tr('commands.content', prefix=self.__cmd_prefix), True) self.reply(self.tr('arguments.title').set_color(TextColors.help_title)) - self.__reply_help(self.tr('arguments.content', export_formats=t_export_formats)) + self.__reply_help(self.tr('arguments.content')) self.reply(self.tr('other.title').set_color(TextColors.help_title)) self.reply(self.tr( @@ -94,6 +93,9 @@ def run(self) -> None: ) else: kwargs['scheduled_compact_notes'] = self.tr(f'node_help.{self.what}.scheduled_compact.off') + elif self.what == 'export': + from prime_backup.types.standalone_backup_format import StandaloneBackupFormat + kwargs['export_formats'] = ', '.join([f'§3{ebf.name}§r' for ebf in StandaloneBackupFormat]) self.__reply_help(self.tr(f'node_help.{self.what}', **kwargs))