-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added
!!pb diff
command to show differences between two backups
- Loading branch information
1 parent
54f452e
commit 940d886
Showing
7 changed files
with
244 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import dataclasses | ||
from typing import List, Dict, Tuple | ||
|
||
from prime_backup.action import Action | ||
from prime_backup.db.access import DbAccess | ||
from prime_backup.db.session import DbSession | ||
from prime_backup.types.file_info import FileInfo | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class DiffResult: | ||
added: List[FileInfo] = dataclasses.field(default_factory=list) | ||
deleted: List[FileInfo] = dataclasses.field(default_factory=list) | ||
changed: List[Tuple[FileInfo, FileInfo]] = dataclasses.field(default_factory=list) # (old, new) | ||
|
||
@property | ||
def diff_count(self) -> int: | ||
return len(self.added) + len(self.changed) + len(self.deleted) | ||
|
||
|
||
class DiffBackupAction(Action[DiffResult]): | ||
def __init__(self, backup_id_old: int, backup_id_new: int, *, compare_status: bool): | ||
super().__init__() | ||
self.backup_id_old = backup_id_old | ||
self.backup_id_new = backup_id_new | ||
self.compare_status = compare_status | ||
|
||
@classmethod | ||
def __get_files_from_backup(cls, session: DbSession, backup_id: int) -> Dict[str, FileInfo]: | ||
files = {} | ||
for file in session.get_backup(backup_id).files: | ||
files[file.path] = FileInfo.of(file) | ||
return files | ||
|
||
def __compare_files(self, a: FileInfo, b: FileInfo) -> bool: | ||
return ( | ||
True | ||
and a.path == b.path | ||
and a.mode == b.mode | ||
and getattr(a.blob, 'hash', None) == getattr(b.blob, 'hash', None) | ||
and a.content == b.content | ||
and (not self.compare_status or ( | ||
True | ||
and a.uid == b.uid | ||
and a.gid == b.gid | ||
and a.mtime_ns == b.mtime_ns | ||
)) | ||
) | ||
|
||
def run(self) -> DiffResult: | ||
with DbAccess.open_session() as session: | ||
files_old = self.__get_files_from_backup(session, self.backup_id_old) | ||
files_new = self.__get_files_from_backup(session, self.backup_id_new) | ||
|
||
result = DiffResult() | ||
for path, file in files_old.items(): | ||
if path in files_new: | ||
new_file = files_new[path] | ||
if not self.__compare_files(file, new_file): | ||
result.changed.append((file, new_file)) | ||
else: | ||
result.deleted.append(file) | ||
for path, file in files_new.items(): | ||
if path not in files_old: | ||
result.added.append(file) | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import stat | ||
from typing import Callable, List | ||
|
||
from mcdreforged.api.all import * | ||
|
||
from prime_backup.action.diff_backup_action import DiffBackupAction | ||
from prime_backup.mcdr.task.basic_task import LightTask | ||
from prime_backup.mcdr.text_components import TextComponents, TextColors | ||
from prime_backup.types.file_info import FileInfo | ||
from prime_backup.utils import conversion_utils | ||
|
||
|
||
class DiffBackupTask(LightTask[None]): | ||
def __init__(self, source: CommandSource, backup_id_old: int, backup_id_new: int): | ||
super().__init__(source) | ||
self.backup_id_old = backup_id_old | ||
self.backup_id_new = backup_id_new | ||
|
||
@property | ||
def id(self) -> str: | ||
return 'backup_diff' | ||
|
||
def run(self) -> None: | ||
result = DiffBackupAction(self.backup_id_old, self.backup_id_new, compare_status=False).run() | ||
|
||
t_bid_old = TextComponents.backup_id(self.backup_id_old) | ||
t_bid_new = TextComponents.backup_id(self.backup_id_new) | ||
t_na = RText('N/A', RColor.gray) | ||
if result.diff_count == 0: | ||
self.reply(self.tr('no_diff', t_bid_old, t_bid_new)) | ||
return | ||
|
||
self.reply(self.tr( | ||
'found_diff', | ||
TextComponents.number(result.diff_count), | ||
t_bid_old, t_bid_new, | ||
RTextBase.join(' ', [ | ||
RText(f'+{len(result.added)}', RColor.green), | ||
RText(f'-{len(result.deleted)}', RColor.red), | ||
RText(f'*{len(result.changed)}', RColor.yellow), | ||
]) | ||
)) | ||
|
||
def pretty_mode(mode: int) -> RTextBase: | ||
if stat.S_ISREG(mode): | ||
type_flag = '-' | ||
color = RColor.light_purple | ||
elif stat.S_ISDIR(mode): | ||
type_flag = 'd' | ||
color = RColor.blue | ||
elif stat.S_ISLNK(mode): | ||
type_flag = 'l' | ||
color = RColor.aqua | ||
else: | ||
type_flag = '?' | ||
color = RColor.gray | ||
|
||
permissions = '' | ||
for i in range(9): | ||
permissions += 'rwx'[i % 3] if (mode >> (8 - i)) & 1 == 1 else '-' | ||
|
||
return RText(type_flag + permissions, color) | ||
|
||
def reply_single(f: FileInfo, head: RTextBase): | ||
text = RTextBase.format( | ||
'{} {} {}', | ||
head, | ||
pretty_mode(file.mode), | ||
RText(file.path, TextColors.file), | ||
) | ||
if f.is_link(): | ||
if f.content is not None: | ||
target = RText(f.content.decode('utf8'), TextColors.file) | ||
else: | ||
target = RText('?', RColor.gray) | ||
text = RTextList(text, ' -> ', target) | ||
self.reply(text) | ||
|
||
for file in sorted(result.added): | ||
reply_single(file, RText('[+]', RColor.green)) | ||
for file in sorted(result.deleted): | ||
reply_single(file, RText('[-]', RColor.red)) | ||
|
||
for old_file, new_file in sorted(result.changed, key=lambda f: f[0]): | ||
def make_hover(what_old, what_new, what_mapper: Callable = lambda x: x): | ||
nonlocal hover_lines | ||
hover_lines = [ | ||
RTextBase.format('{}: {}', t_bid_old, what_mapper(what_old) if what_old is not None else t_na), | ||
RTextBase.format('{}: {}', t_bid_new, what_mapper(what_new) if what_new is not None else t_na), | ||
] | ||
|
||
def map_or_none(value, maper: Callable): | ||
return maper(value) if value is not None else None | ||
|
||
hover_lines: List[RTextBase] = [] | ||
if old_file.mode != new_file.mode: | ||
t_change = self.tr('diff.mode') | ||
make_hover(pretty_mode(old_file.mode), pretty_mode(new_file.mode)) | ||
elif (h1 := map_or_none(old_file.blob, lambda b: b.hash)) != (h2 := map_or_none(new_file.blob, lambda b: b.hash)): | ||
t_change = self.tr('diff.blob') | ||
n = 8 | ||
if h1 is not None and h2 is not None: | ||
while n < min(len(h1), len(h2)): | ||
if h1[:n] != h2[:n]: | ||
break | ||
n += 8 | ||
make_hover(h1, h2, lambda h: h[:n]) | ||
elif old_file.content != new_file.content: | ||
# currently only symlink uses the content | ||
t_change = self.tr('diff.link_target') | ||
make_hover(old_file.content, new_file.content, lambda t: t.decode('utf8')) | ||
elif old_file.uid != new_file.uid or old_file.gid != new_file.gid: | ||
def format_owner(f: FileInfo): | ||
return RTextBase.format('uid={} gid={}', TextComponents.number(f.uid), TextComponents.number(f.gid)) | ||
t_change = self.tr('diff.owner') | ||
make_hover(format_owner(old_file), format_owner(new_file)) | ||
elif old_file.mtime_ns != new_file.mtime_ns: | ||
t_change = self.tr('diff.mtime') | ||
make_hover(old_file.mtime_ns, new_file.mtime_ns, lambda mt: TextComponents.date(conversion_utils.timestamp_to_local_date(mt))) | ||
else: | ||
t_change = self.tr('diff.other').set_color(RColor.gray) | ||
|
||
if len(hover_lines) > 0 and self.source.has_permission(PermissionLevel.PHYSICAL_SERVER_CONTROL_LEVEL): | ||
if self.source.is_player: | ||
t_change.h(RTextBase.join('\n', hover_lines)) | ||
else: | ||
t_change = RTextList(t_change, ' (', RTextBase.join(', ', hover_lines), ')') | ||
|
||
self.reply(RTextBase.format( | ||
'{} {} {}: {}', | ||
RText('[*]', RColor.yellow), | ||
pretty_mode(new_file.mode), | ||
RText(old_file.path, TextColors.file), | ||
t_change, | ||
)) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters