1
0
Fork 0
mirror of https://github.com/borgbase/vorta synced 2025-01-03 05:36:19 +00:00

Handle ctime and mtime diff changes (#1675)

Borg v1.2.4 added new change types called `mtime` and `ctime` for the modification and the creation time of a file.
Our diff json parser doesn't support these changes yet.
The plain text parser doesn't need to be updated since it is only used for earlier versions of borg.
This also extends the tooltip in the diff view to show changes in `ctime` or `mtime` in a localised manner.

* src/vorta/views/diff_result.py (ChangeType): Add `CTIME` and `MTIME` linking to `MODIFIED`.

* src/vorta/views/diff_result.py (DiffData): Add fields `ctime_change` and `mtime_change`.

* src/vorta/views/diff_result.py (parse_diff_json): Parse the new change types.

* src/vorta/views/diff_result.py (DiffTree.data): Add time changes to tooltip in a human readable format.

* tests/test_diff.py : Update test data to include new change types. Add additional test cases for unittesting the new change types.
This commit is contained in:
Henry Spanka 2023-04-01 21:10:56 +02:00 committed by real-yfprojects
parent f407032a76
commit e3451ed49e
No known key found for this signature in database
GPG key ID: 00F630DFDEE25747
2 changed files with 217 additions and 14 deletions

View file

@ -6,7 +6,7 @@
from pathlib import PurePath from pathlib import PurePath
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from PyQt5 import uic from PyQt5 import uic
from PyQt5.QtCore import QMimeData, QModelIndex, QPoint, Qt, QThread, QUrl from PyQt5.QtCore import QDateTime, QLocale, QMimeData, QModelIndex, QPoint, Qt, QThread, QUrl
from PyQt5.QtGui import QColor, QKeySequence from PyQt5.QtGui import QColor, QKeySequence
from PyQt5.QtWidgets import QApplication, QHeaderView, QMenu, QShortcut, QTreeView from PyQt5.QtWidgets import QApplication, QHeaderView, QMenu, QShortcut, QTreeView
from vorta.utils import get_asset, pretty_bytes, uses_dark_mode from vorta.utils import get_asset, pretty_bytes, uses_dark_mode
@ -206,6 +206,8 @@ def parse_diff_json(diffs: List[dict], model: 'DiffTree'):
change_type: ChangeType = None change_type: ChangeType = None
mode_change: Optional[Tuple[str, str]] = None mode_change: Optional[Tuple[str, str]] = None
owner_change: Optional[Tuple[str, str, str, str]] = None owner_change: Optional[Tuple[str, str, str, str]] = None
ctime_change: Optional[Tuple[QDateTime, QDateTime]] = None
mtime_change: Optional[Tuple[QDateTime, QDateTime]] = None
modified: Optional[Tuple[int, int]] = None modified: Optional[Tuple[int, int]] = None
# added link, removed link, changed link # added link, removed link, changed link
@ -213,6 +215,8 @@ def parse_diff_json(diffs: List[dict], model: 'DiffTree'):
# added directory, removed directory # added directory, removed directory
# owner (old_user, new_user, old_group, new_group)) # owner (old_user, new_user, old_group, new_group))
# mode (old_mode, new_mode) # mode (old_mode, new_mode)
# ctime (old_ctime, new_ctime)
# mtime (old_mtime, new_mtime)
for change in item['changes']: for change in item['changes']:
# if more than one type of change has happened for this file/dir/link, then report the most important # if more than one type of change has happened for this file/dir/link, then report the most important
# (higher priority) # (higher priority)
@ -269,6 +273,22 @@ def parse_diff_json(diffs: List[dict], model: 'DiffTree'):
change['new_user'], change['new_user'],
change['new_group'], change['new_group'],
) )
elif change['type'] == 'ctime':
# ctime change can occur along with previous changes
change_type = ChangeType.MODIFIED
ctime_change = (
QDateTime.fromString(change['old_ctime'], Qt.DateFormat.ISODateWithMs),
QDateTime.fromString(change['new_ctime'], Qt.DateFormat.ISODateWithMs),
)
elif change['type'] == 'mtime':
# mtime change can occur along with previous changes
change_type = ChangeType.MODIFIED
mtime_change = (
QDateTime.fromString(change['old_mtime'], Qt.DateFormat.ISODateWithMs),
QDateTime.fromString(change['new_mtime'], Qt.DateFormat.ISODateWithMs),
)
else: else:
raise Exception('Unknown change type: {}'.format(change['type'])) raise Exception('Unknown change type: {}'.format(change['type']))
@ -282,6 +302,8 @@ def parse_diff_json(diffs: List[dict], model: 'DiffTree'):
size=size, size=size,
mode_change=mode_change, mode_change=mode_change,
owner_change=owner_change, owner_change=owner_change,
ctime_change=ctime_change,
mtime_change=mtime_change,
modified=modified, modified=modified,
), ),
) )
@ -492,6 +514,8 @@ class ChangeType(enum.Enum):
such as - a file is deleted and replaced with such as - a file is deleted and replaced with
a directory of the same name. a directory of the same name.
owner - user and/or group ownership changed. owner - user and/or group ownership changed.
ctime - creation time changed.
mtime - modification time changed.
size: size:
If type == `added` or `removed`, If type == `added` or `removed`,
@ -518,6 +542,14 @@ class ChangeType(enum.Enum):
See old_user property. See old_user property.
new_group: new_group:
See old_user property. See old_user property.
old_ctime:
If type == `ctime`, then old_ctime and new_ctime provide creation time changes.
new_ctime:
See old_ctime property.
old_mtime:
If type == `mtime`, then old_mtime and new_mtime provide modification time changes.
new_mtime:
See old_mtime property.
""" """
NONE = 0 # no change NONE = 0 # no change
@ -531,6 +563,8 @@ class ChangeType(enum.Enum):
CHANGED_LINK = MODIFIED CHANGED_LINK = MODIFIED
MODE = MODIFIED # changed permissions MODE = MODIFIED # changed permissions
OWNER = MODIFIED OWNER = MODIFIED
CTIME = MODIFIED
MTIME = MODIFIED
def short(self): def short(self):
"""Get a short identifier for the change type.""" """Get a short identifier for the change type."""
@ -588,6 +622,8 @@ class DiffData:
size: int # size change (disk usage) size: int # size change (disk usage)
mode_change: Optional[Tuple[str, str]] = None mode_change: Optional[Tuple[str, str]] = None
owner_change: Optional[Tuple[str, str, str, str]] = None owner_change: Optional[Tuple[str, str, str, str]] = None
ctime_change: Optional[Tuple[QDateTime, QDateTime]] = None
mtime_change: Optional[Tuple[QDateTime, QDateTime]] = None
modified: Optional[Tuple[int, int]] = None modified: Optional[Tuple[int, int]] = None
@ -798,6 +834,7 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
modified_template = self.tr("Added {}, deleted {}") modified_template = self.tr("Added {}, deleted {}")
owner_template = "{: <10} -> {: >10}" owner_template = "{: <10} -> {: >10}"
time_template = "{}: {} -> {}"
permission_template = "{} -> {}" permission_template = "{} -> {}"
# format # format
@ -844,4 +881,20 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
"{}:{}".format(item.data.owner_change[2], item.data.owner_change[3]), "{}:{}".format(item.data.owner_change[2], item.data.owner_change[3]),
) )
if item.data.ctime_change:
tooltip += '\n'
tooltip += time_template.format(
"Creation Time",
QLocale.system().toString(item.data.ctime_change[0], QLocale.FormatType.ShortFormat),
QLocale.system().toString(item.data.ctime_change[1], QLocale.FormatType.ShortFormat),
)
if item.data.mtime_change:
tooltip += '\n'
tooltip += time_template.format(
"Modification Time",
QLocale.system().toString(item.data.mtime_change[0], QLocale.FormatType.ShortFormat),
QLocale.system().toString(item.data.mtime_change[1], QLocale.FormatType.ShortFormat),
)
return tooltip return tooltip

View file

@ -1,6 +1,6 @@
from pathlib import PurePath from pathlib import PurePath
import pytest import pytest
from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtCore import QDateTime, QItemSelectionModel, Qt
import vorta.borg import vorta.borg
import vorta.utils import vorta.utils
import vorta.views.archive_tab import vorta.views.archive_tab
@ -56,11 +56,22 @@ def check(feature_name):
[ [
( (
'changed link some/changed/link', 'changed link some/changed/link',
('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None), ('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None, None, None),
), ),
( (
' +77.8 kB -77.8 kB some/changed/file', ' +77.8 kB -77.8 kB some/changed/file',
('some/changed/file', FileType.FILE, ChangeType.MODIFIED, 2 * 77800, 0, None, None, (77800, 77800)), (
'some/changed/file',
FileType.FILE,
ChangeType.MODIFIED,
2 * 77800,
0,
None,
None,
None,
None,
(77800, 77800),
),
), ),
( (
' +77.8 kB -77.8 kB [-rw-rw-rw- -> -rw-r--r--] some/changed/file', ' +77.8 kB -77.8 kB [-rw-rw-rw- -> -rw-r--r--] some/changed/file',
@ -72,20 +83,33 @@ def check(feature_name):
0, 0,
('-rw-rw-rw-', '-rw-r--r--'), ('-rw-rw-rw-', '-rw-r--r--'),
None, None,
None,
None,
(77800, 77800), (77800, 77800),
), ),
), ),
( (
'[-rw-rw-rw- -> -rw-r--r--] some/changed/file', '[-rw-rw-rw- -> -rw-r--r--] some/changed/file',
('some/changed/file', FileType.FILE, ChangeType.MODE, 0, 0, ('-rw-rw-rw-', '-rw-r--r--'), None, None), (
'some/changed/file',
FileType.FILE,
ChangeType.MODE,
0,
0,
('-rw-rw-rw-', '-rw-r--r--'),
None,
None,
None,
None,
),
), ),
( (
'added directory some/changed/dir', 'added directory some/changed/dir',
('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None), ('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None, None, None),
), ),
( (
'removed directory some/changed/dir', 'removed directory some/changed/dir',
('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None), ('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None, None, None),
), ),
# Example from https://github.com/borgbase/vorta/issues/521 # Example from https://github.com/borgbase/vorta/issues/521
( (
@ -99,12 +123,25 @@ def check(feature_name):
None, None,
('user', 'user', 'nfsnobody', 'nfsnobody'), ('user', 'user', 'nfsnobody', 'nfsnobody'),
None, None,
None,
None,
), ),
), ),
# Very short owner change, to check stripping whitespace from file path # Very short owner change, to check stripping whitespace from file path
( (
'[a:a -> b:b] home/user/arrays/test.txt', '[a:a -> b:b] home/user/arrays/test.txt',
('home/user/arrays/test.txt', FileType.FILE, ChangeType.OWNER, 0, 0, None, ('a', 'a', 'b', 'b'), None), (
'home/user/arrays/test.txt',
FileType.FILE,
ChangeType.OWNER,
0,
0,
None,
('a', 'a', 'b', 'b'),
None,
None,
None,
),
), ),
# All file-related changes in one test # All file-related changes in one test
( (
@ -117,6 +154,8 @@ def check(feature_name):
77000, 77000,
('-rw-rw-rw-', '-rw-r--r--'), ('-rw-rw-rw-', '-rw-r--r--'),
('user', 'user', 'nfsnobody', 'nfsnobody'), ('user', 'user', 'nfsnobody', 'nfsnobody'),
None,
None,
(77800, 800), (77800, 800),
), ),
), ),
@ -139,11 +178,22 @@ def test_archive_diff_parser(line, expected):
[ [
( (
{'path': 'some/changed/link', 'changes': [{'type': 'changed link'}]}, {'path': 'some/changed/link', 'changes': [{'type': 'changed link'}]},
('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None), ('some/changed/link', FileType.LINK, ChangeType.CHANGED_LINK, 0, 0, None, None, None, None, None),
), ),
( (
{'path': 'some/changed/file', 'changes': [{'type': 'modified', 'added': 77800, 'removed': 77800}]}, {'path': 'some/changed/file', 'changes': [{'type': 'modified', 'added': 77800, 'removed': 77800}]},
('some/changed/file', FileType.FILE, ChangeType.MODIFIED, 2 * 77800, 0, None, None, (77800, 77800)), (
'some/changed/file',
FileType.FILE,
ChangeType.MODIFIED,
2 * 77800,
0,
None,
None,
None,
None,
(77800, 77800),
),
), ),
( (
{ {
@ -161,6 +211,8 @@ def test_archive_diff_parser(line, expected):
77000, 77000,
('-rw-rw-rw-', '-rw-r--r--'), ('-rw-rw-rw-', '-rw-r--r--'),
None, None,
None,
None,
(77800, 800), (77800, 800),
), ),
), ),
@ -169,15 +221,26 @@ def test_archive_diff_parser(line, expected):
'path': 'some/changed/file', 'path': 'some/changed/file',
'changes': [{'type': 'mode', 'old_mode': '-rw-rw-rw-', 'new_mode': '-rw-r--r--'}], 'changes': [{'type': 'mode', 'old_mode': '-rw-rw-rw-', 'new_mode': '-rw-r--r--'}],
}, },
('some/changed/file', FileType.FILE, ChangeType.MODE, 0, 0, ('-rw-rw-rw-', '-rw-r--r--'), None, None), (
'some/changed/file',
FileType.FILE,
ChangeType.MODE,
0,
0,
('-rw-rw-rw-', '-rw-r--r--'),
None,
None,
None,
None,
),
), ),
( (
{'path': 'some/changed/dir', 'changes': [{'type': 'added directory'}]}, {'path': 'some/changed/dir', 'changes': [{'type': 'added directory'}]},
('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None), ('some/changed/dir', FileType.DIRECTORY, ChangeType.ADDED, 0, 0, None, None, None, None, None),
), ),
( (
{'path': 'some/changed/dir', 'changes': [{'type': 'removed directory'}]}, {'path': 'some/changed/dir', 'changes': [{'type': 'removed directory'}]},
('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None), ('some/changed/dir', FileType.DIRECTORY, ChangeType.REMOVED_DIR, 0, 0, None, None, None, None, None),
), ),
# Example from https://github.com/borgbase/vorta/issues/521 # Example from https://github.com/borgbase/vorta/issues/521
( (
@ -202,6 +265,8 @@ def test_archive_diff_parser(line, expected):
None, None,
('user', 'user', 'nfsnobody', 'nfsnobody'), ('user', 'user', 'nfsnobody', 'nfsnobody'),
None, None,
None,
None,
), ),
), ),
# Very short owner change, to check stripping whitespace from file path # Very short owner change, to check stripping whitespace from file path
@ -210,7 +275,74 @@ def test_archive_diff_parser(line, expected):
'path': 'home/user/arrays/test.txt', 'path': 'home/user/arrays/test.txt',
'changes': [{'type': 'owner', 'old_user': 'a', 'new_user': 'b', 'old_group': 'a', 'new_group': 'b'}], 'changes': [{'type': 'owner', 'old_user': 'a', 'new_user': 'b', 'old_group': 'a', 'new_group': 'b'}],
}, },
('home/user/arrays/test.txt', FileType.FILE, ChangeType.OWNER, 0, 0, None, ('a', 'a', 'b', 'b'), None), (
'home/user/arrays/test.txt',
FileType.FILE,
ChangeType.OWNER,
0,
0,
None,
('a', 'a', 'b', 'b'),
None,
None,
None,
),
),
# Short ctime change
(
{
'path': 'home/user/arrays',
'changes': [
{
'new_ctime': '2023-04-01T17:23:14.104630',
'old_ctime': '2023-03-03T23:40:17.073948',
'type': 'ctime',
}
],
},
(
'home/user/arrays',
FileType.FILE,
ChangeType.MODIFIED,
0,
0,
None,
None,
(
QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
),
None,
None,
),
),
# Short mtime change
(
{
'path': 'home/user/arrays',
'changes': [
{
'new_mtime': '2023-04-01T17:23:14.104630',
'old_mtime': '2023-03-03T23:40:17.073948',
'type': 'mtime',
}
],
},
(
'home/user/arrays',
FileType.FILE,
ChangeType.MODIFIED,
0,
0,
None,
None,
None,
(
QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
),
None,
),
), ),
# All file-related changes in one test # All file-related changes in one test
( (
@ -226,6 +358,16 @@ def test_archive_diff_parser(line, expected):
'old_group': 'user', 'old_group': 'user',
'new_group': 'nfsnobody', 'new_group': 'nfsnobody',
}, },
{
'new_ctime': '2023-04-01T17:23:14.104630',
'old_ctime': '2023-03-03T23:40:17.073948',
'type': 'ctime',
},
{
'new_mtime': '2023-04-01T17:15:50.290565',
'old_mtime': '2023-03-05T00:24:00.359045',
'type': 'mtime',
},
], ],
}, },
( (
@ -236,6 +378,14 @@ def test_archive_diff_parser(line, expected):
0, 0,
('-rw-rw-rw-', '-rw-r--r--'), ('-rw-rw-rw-', '-rw-r--r--'),
('user', 'user', 'nfsnobody', 'nfsnobody'), ('user', 'user', 'nfsnobody', 'nfsnobody'),
(
QDateTime.fromString('2023-03-03T23:40:17.073948', Qt.DateFormat.ISODateWithMs),
QDateTime.fromString('2023-04-01T17:23:14.104630', Qt.DateFormat.ISODateWithMs),
),
(
QDateTime.fromString('2023-03-05T00:24:00.359045', Qt.DateFormat.ISODateWithMs),
QDateTime.fromString('2023-04-01T17:15:50.290565', Qt.DateFormat.ISODateWithMs),
),
(77800, 77800), (77800, 77800),
), ),
), ),