Skip to content

Commit

Permalink
add support for 'diff --json-lines' coming real soon to borg.
Browse files Browse the repository at this point in the history
fixes issue #901 (diff not handling CR/LF in filenames)
  • Loading branch information
Robert Blenis committed Mar 2, 2021
1 parent c66b102 commit 0f0c941
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 23 deletions.
3 changes: 2 additions & 1 deletion src/vorta/borg/_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
MIN_BORG_FOR_FEATURE = {
'BLAKE2': parse_version('1.1.4'),
'ZSTD': parse_version('1.1.4'),
'JSON_LOG': parse_version('1.1.0')
'JSON_LOG': parse_version('1.1.0'),
'DIFF_JSON_LINES': parse_version('1.1.16')
# add new version-checks here.
}

Expand Down
21 changes: 11 additions & 10 deletions src/vorta/borg/diff.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .borg_thread import BorgThread
from vorta.utils import borg_compat


class BorgDiffThread(BorgThread):
Expand All @@ -17,17 +18,17 @@ def prepare(cls, profile, archive_name_1, archive_name_2):
ret = super().prepare(profile)
if not ret['ok']:
return ret
else:
ret['ok'] = False # Set back to false, so we can do our own checks here.

cmd = ['borg', 'diff', '--info', '--log-json']
cmd.append(f'{profile.repo.url}::{archive_name_1}')
cmd.append(f'{archive_name_2}')

ret['cmd'] = [
'borg', 'diff', '--info', '--log-json',
f'{profile.repo.url}::{archive_name_1}',
f'{archive_name_2}']
ret['ok'] = True
ret['cmd'] = cmd
ret['archive_name_older'] = archive_name_1
ret['archive_name_newer'] = archive_name_2
ret['json-lines'] = False
if borg_compat.check('DIFF_JSON_LINES'):
ret['cmd'].insert(4, '--json-lines')
ret['json-lines'] = True

return ret

def process_result(self, result):
pass
4 changes: 1 addition & 3 deletions src/vorta/views/archive_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,6 @@ def process_result():
archive_name_older = archive_cell_older.text()

params = BorgDiffThread.prepare(profile, archive_name_older, archive_name_newer)
params['archive_name_newer'] = archive_name_newer
params['archive_name_older'] = archive_name_older

if params['ok']:
self._toggle_all_buttons(False)
Expand All @@ -517,7 +515,7 @@ def list_diff_result(self, result):
if result['returncode'] == 0:
archive_newer = ArchiveModel.get(name=result['params']['archive_name_newer'])
archive_older = ArchiveModel.get(name=result['params']['archive_name_older'])
window = DiffResult(result['data'], archive_newer, archive_older)
window = DiffResult(result['data'], archive_newer, archive_older, result['params']['json-lines'])
self._toggle_all_buttons(True)
window.setParent(self, QtCore.Qt.Sheet)
self._resultwindow = window # for testing
Expand Down
102 changes: 93 additions & 9 deletions src/vorta/views/diff_result.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import re

Expand All @@ -15,11 +16,19 @@


class DiffResult(DiffResultBase, DiffResultUI):
def __init__(self, fs_data, archive_newer, archive_older):
def __init__(self, fs_data, archive_newer, archive_older, json_lines):
super().__init__()
self.setupUi(self)

files_with_attributes, nested_file_list = parse_diff_lines(fs_data.split('\n'))
if json_lines:
# handle case of a single line of result, which will already be a dict
lines = [fs_data] if isinstance(fs_data, dict) else \
[json.loads(line) for line in fs_data.split('\n') if line]
else:
lines = [line for line in fs_data.split('\n') if line]

files_with_attributes, nested_file_list = parse_diff_json_lines(lines) \
if json_lines else parse_diff_lines(lines)
model = DiffTree(files_with_attributes, nested_file_list)

view = self.treeView
Expand All @@ -37,15 +46,62 @@ def __init__(self, fs_data, archive_newer, archive_older):
self.okButton.clicked.connect(self.accept)


def parse_diff_lines(diff_lines):
def parse_diff_json_lines(diffs):
files_with_attributes = []
nested_file_list = nested_dict()

for item in diffs:
dirpath, name = os.path.split(item['path'])

# add to nested dict of folders to find nested dirs.
d = get_dict_from_list(nested_file_list, dirpath.split('/'))
if name not in d:
d[name] = {}

# added link, removed link, changed link
# modified (added, removed), added (size), removed (size)
# added directory, removed directory
# owner (olduser, newuser, oldgroup, newgroup))
# mode (oldmode, newmode)
size = 0
change_type = None
change_type_priority = 0
for change in item['changes']:
if {'type': 'modified'} == change:
# modified, but can't compare ids
change_type = 'modified'
change_type_priority = 2
elif change['type'] == 'modified':
change_type = '{:>9} {:>9}'.format(format_file_size(change['added'], precision=1, sign=True),
format_file_size(-change['removed'], precision=1, sign=True))
size = change['added'] - change['removed']
change_type_priority = 2
elif change['type'] in ['added', 'removed', 'added link', 'removed link', 'changed link',
'added directory', 'removed directory']:
change_type = change['type'].split()[0] # 'added', 'removed' or 'changed'
size = change.get('size', 0)
change_type_priority = 2
elif change['type'] == 'mode' and change_type_priority < 2:
# mode change can occur along with previous changes - don't override
change_type = '{} --> {}'.format(change['oldmode'], change['newmode'])
change_type_priority = 1
elif change['type'] == 'owner' and change_type_priority < 1:
# owner change can occur along with previous changes - don't override
change_type = '{}:{} -> {}:{}'.format(change['olduser'], change['oldgroup'],
change['newuser'], change['newgroup'])
# should only get here if multiple changes occured, otherwise unrecognized change type
assert change_type

files_with_attributes.append((size, change_type, name, dirpath))

return (files_with_attributes, nested_file_list)


def parse_diff_lines(diff_lines):
nested_file_list = nested_dict()

def parse_line(line):
if line:
line_split = line.split()
else:
return 0, "", "", ""
line_split = line.split()

if line_split[0] in {'added', 'removed', 'changed'}:
change_type = line_split[0]
Expand Down Expand Up @@ -90,8 +146,7 @@ def parse_line(line):

return size, change_type, name, dir

for line in diff_lines:
files_with_attributes.append(parse_line(line))
files_with_attributes = [parse_line(line) for line in diff_lines if line]

return (files_with_attributes, nested_file_list)

Expand All @@ -109,6 +164,35 @@ def calc_size(significand, unit):
return int(float(significand) * 10**12)


def sizeof_fmt(num, suffix='B', units=None, power=None, sep='', precision=2, sign=False):
prefix = '+' if sign and num > 0 else ''

for unit in units[:-1]:
if abs(round(num, precision)) < power:
if isinstance(num, int):
return "{}{}{}{}{}".format(prefix, num, sep, unit, suffix)
else:
return "{}{:3.{}f}{}{}{}".format(prefix, num, precision, sep, unit, suffix)
num /= float(power)
return "{}{:.{}f}{}{}{}".format(prefix, num, precision, sep, units[-1], suffix)


def sizeof_fmt_iec(num, suffix='B', sep='', precision=2, sign=False):
return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, sign=sign,
units=['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'], power=1024)


def sizeof_fmt_decimal(num, suffix='B', sep='', precision=2, sign=False):
return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, sign=sign,
units=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], power=1000)


def format_file_size(v, precision=2, sign=False):
"""Format file size into a human friendly format
"""
return sizeof_fmt_decimal(v, suffix='B', sep=' ', precision=precision, sign=sign)


class DiffTree(TreeModel):
def __init__(self, files_with_attributes, nested_file_list, parent=None,):
super().__init__(
Expand Down

0 comments on commit 0f0c941

Please sign in to comment.