mirror of https://github.com/borgbase/vorta
Rewrite of borg diff core (#373)
- It now heavily relies on regex. This was done in order to make the code more stable and easier to maintain. - Share common code for file tree view between extract and diff.
This commit is contained in:
parent
4a8d6e8768
commit
3b6c5f2d3c
|
@ -76,6 +76,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
|
||||
self.populate_from_profile()
|
||||
|
||||
self.selected_archives = None
|
||||
|
||||
def _set_status(self, text):
|
||||
self.mountErrors.setText(text)
|
||||
self.mountErrors.repaint()
|
||||
|
@ -429,9 +431,10 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
window.show()
|
||||
|
||||
if window.exec_():
|
||||
selected_archives = window.selected_archives
|
||||
archive_cell_newer = self.archiveTable.item(selected_archives[0], 4)
|
||||
archive_cell_older = self.archiveTable.item(selected_archives[1], 4)
|
||||
if window.selected_archives:
|
||||
self.selected_archives = window.selected_archives
|
||||
archive_cell_newer = self.archiveTable.item(self.selected_archives[0], 4)
|
||||
archive_cell_older = self.archiveTable.item(self.selected_archives[1], 4)
|
||||
if archive_cell_older and archive_cell_newer:
|
||||
archive_name_newer = archive_cell_newer.text()
|
||||
archive_name_older = archive_cell_older.text()
|
||||
|
@ -457,5 +460,5 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
window = DiffResult(result['data'], archive_newer, archive_older)
|
||||
self._toggle_all_buttons(True)
|
||||
window.setParent(self, QtCore.Qt.Sheet)
|
||||
self._window = window # for testing
|
||||
self._resultwindow = window # for testing
|
||||
window.show()
|
||||
|
|
|
@ -3,12 +3,11 @@ from PyQt5.QtWidgets import QHeaderView, QTableView, QTableWidgetItem
|
|||
|
||||
from vorta.utils import get_asset
|
||||
|
||||
uifile = get_asset('UI/diffdialog.ui')
|
||||
uifile = get_asset("UI/diffdialog.ui")
|
||||
DiffDialogUI, DiffDialogBase = uic.loadUiType(uifile)
|
||||
|
||||
|
||||
class DiffDialog(DiffDialogBase, DiffDialogUI):
|
||||
|
||||
def __init__(self, archiveTable):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
|
@ -38,7 +37,7 @@ class DiffDialog(DiffDialogBase, DiffDialogUI):
|
|||
text = archiveTable.item(row, column).text()
|
||||
self.archiveTable.setItem(row, column, QTableWidgetItem(text))
|
||||
except AttributeError:
|
||||
self.archiveTable.setItem(row, column, QTableWidgetItem(''))
|
||||
self.archiveTable.setItem(row, column, QTableWidgetItem(""))
|
||||
|
||||
self.diffButton.setEnabled(False)
|
||||
|
||||
|
|
|
@ -1,34 +1,28 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, QVariant
|
||||
from PyQt5.QtCore import Qt, QVariant
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtWidgets import QHeaderView
|
||||
|
||||
from vorta.utils import (get_asset, get_dict_from_list, nested_dict,
|
||||
pretty_bytes)
|
||||
from vorta.utils import (get_asset, get_dict_from_list, nested_dict)
|
||||
|
||||
from vorta.views.tree_view import TreeModel
|
||||
|
||||
uifile = get_asset('UI/diffresult.ui')
|
||||
DiffResultUI, DiffResultBase = uic.loadUiType(uifile)
|
||||
|
||||
files_with_attributes = None
|
||||
nested_file_list = None
|
||||
selected_files_folders = None
|
||||
|
||||
|
||||
class DiffResult(DiffResultBase, DiffResultUI):
|
||||
def __init__(self, fs_data, archive_newer, archive_older):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
global files_with_attributes, nested_file_list, selected_files_folders
|
||||
|
||||
# Clear global file lists
|
||||
files_with_attributes = []
|
||||
nested_file_list = nested_dict()
|
||||
selected_files_folders = set()
|
||||
|
||||
def parse_line(line):
|
||||
|
||||
if line:
|
||||
line_split = line.split()
|
||||
else:
|
||||
|
@ -36,59 +30,48 @@ class DiffResult(DiffResultBase, DiffResultUI):
|
|||
|
||||
if line_split[0] == 'added' or line_split[0] == 'removed':
|
||||
change_type = line_split[0]
|
||||
size = line_split[1]
|
||||
unit = line_split[2]
|
||||
if line_split[1] in ['directory', 'link']:
|
||||
size = 0
|
||||
full_path = re.search(r'^\w+ \w+ +(.*)', line).group(1)
|
||||
else:
|
||||
significand = line_split[1]
|
||||
unit = line_split[2]
|
||||
size = calc_size(significand, unit)
|
||||
full_path = re.search(r'^\w+ +\S+ \w?B (.*)', line).group(1)
|
||||
else:
|
||||
change_type = "modified"
|
||||
size = line_split[0]
|
||||
unit = line_split[1]
|
||||
# If present remove '+' or '-' sign at the front
|
||||
if size[0] in ('+', '-'):
|
||||
size = size[1:]
|
||||
size_change = re.search(r' *[\+-]?(\d+\.*\d*) (\w?B) +[\+-]?.+\w?B ', line)
|
||||
if size_change:
|
||||
significand = size_change.group(1)
|
||||
unit = size_change.group(2)
|
||||
size = calc_size(significand, unit)
|
||||
full_path_index = size_change.end(0)
|
||||
else:
|
||||
size = 0
|
||||
|
||||
if line_split[0].startswith("["):
|
||||
size = 0
|
||||
change_type = line[:line.find(line_split[3])]
|
||||
full_path = line[line.find(line_split[3]):]
|
||||
dir, name = os.path.split(full_path)
|
||||
# add to nested dict of folders to find nested dirs.
|
||||
d = get_dict_from_list(nested_file_list, full_path.split('/'))
|
||||
elif line_split[1] not in ['directory', 'link']:
|
||||
if unit == 'B':
|
||||
size = int(size)
|
||||
elif unit == 'kB':
|
||||
size = int(float(size) * 10**3)
|
||||
elif unit == 'MB':
|
||||
size = int(float(size) * 10**6)
|
||||
elif unit == 'GB':
|
||||
size = int(float(size) * 10**9)
|
||||
elif unit == 'TB':
|
||||
size = int(float(size) * 10**12)
|
||||
permission_change = re.search(r' *(\[.{24}\]) ', line)
|
||||
if permission_change:
|
||||
change_type = permission_change.group(1)
|
||||
full_path_index = permission_change.end(0)
|
||||
else:
|
||||
change_type = "modified"
|
||||
|
||||
if change_type == 'added' or change_type == 'removed':
|
||||
full_path = line[line.find(line_split[3]):]
|
||||
elif change_type == "modified":
|
||||
full_path = line[line.find(line_split[4]):]
|
||||
if size_change and permission_change:
|
||||
full_path_index = max(size_change.end(0), permission_change.end(0))
|
||||
full_path = line[full_path_index:]
|
||||
|
||||
dir, name = os.path.split(full_path)
|
||||
# add to nested dict of folders to find nested dirs.
|
||||
d = get_dict_from_list(nested_file_list, dir.split('/'))
|
||||
if name not in d:
|
||||
d[name] = {}
|
||||
else:
|
||||
size = 0
|
||||
full_path = line[line.find(line_split[2]):]
|
||||
dir, name = os.path.split(full_path)
|
||||
|
||||
dir, name = os.path.split(full_path)
|
||||
# add to nested dict of folders to find nested dirs.
|
||||
d = get_dict_from_list(nested_file_list, full_path.split('/'))
|
||||
# add to nested dict of folders to find nested dirs.
|
||||
d = get_dict_from_list(nested_file_list, dir.split('/'))
|
||||
if name not in d:
|
||||
d[name] = {}
|
||||
|
||||
return size, change_type, name, dir
|
||||
|
||||
for l in fs_data.split('\n'):
|
||||
files_with_attributes.append(parse_line(l))
|
||||
|
||||
model = TreeModel()
|
||||
model = DiffTree(files_with_attributes, nested_file_list)
|
||||
|
||||
view = self.treeView
|
||||
view.setAlternatingRowColors(True)
|
||||
|
@ -103,113 +86,31 @@ class DiffResult(DiffResultBase, DiffResultUI):
|
|||
self.archiveNameLabel_1.setText(f'{archive_newer.name}')
|
||||
self.archiveNameLabel_2.setText(f'{archive_older.name}')
|
||||
self.okButton.clicked.connect(self.accept)
|
||||
self.selected = selected_files_folders
|
||||
|
||||
|
||||
class FolderItem:
|
||||
def __init__(self, path, name, modified, parent=None):
|
||||
self.parentItem = parent
|
||||
self.path = path
|
||||
self.itemData = [name, modified]
|
||||
self.childItems = []
|
||||
|
||||
# Pre-filter children
|
||||
self._filtered_children = []
|
||||
search_path = os.path.join(self.path, name)
|
||||
if parent is None: # Find path for root folder
|
||||
for root_folder in nested_file_list.keys():
|
||||
self._filtered_children.append((0, '', root_folder, '', ))
|
||||
else:
|
||||
|
||||
# This adds direct children.
|
||||
self._filtered_children = [f for f in files_with_attributes if search_path == f[3]]
|
||||
|
||||
# Add nested folders.
|
||||
for immediate_child in get_dict_from_list(nested_file_list, search_path.split('/')).keys():
|
||||
if not [True for child in self._filtered_children if child[2] == immediate_child]:
|
||||
self._filtered_children.append((0, '', immediate_child, search_path))
|
||||
|
||||
self.is_loaded = False
|
||||
|
||||
def load_children(self):
|
||||
for child_item in self._filtered_children:
|
||||
if child_item[0] > 0: # This is a file
|
||||
self.childItems.append(FileItem(
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
size=child_item[0],
|
||||
parent=self))
|
||||
else: # Folder
|
||||
self.childItems.append(
|
||||
FolderItem(
|
||||
path=child_item[3],
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
parent=self))
|
||||
self.is_loaded = True
|
||||
|
||||
def child(self, row):
|
||||
return self.childItems[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self._filtered_children)
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column <= 1:
|
||||
return self.itemData[column]
|
||||
else:
|
||||
return None
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
if self.parentItem:
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
return 0
|
||||
def calc_size(significand, unit):
|
||||
if unit == 'B':
|
||||
return int(significand)
|
||||
elif unit == 'kB':
|
||||
return int(float(significand) * 10**3)
|
||||
elif unit == 'MB':
|
||||
return int(float(significand) * 10**6)
|
||||
elif unit == 'GB':
|
||||
return int(float(significand) * 10**9)
|
||||
elif unit == 'TB':
|
||||
return int(float(significand) * 10**12)
|
||||
|
||||
|
||||
class FileItem(FolderItem):
|
||||
def __init__(self, name, modified, size, parent=None):
|
||||
self.parentItem = parent
|
||||
self.itemData = [name, modified, size]
|
||||
|
||||
def childCount(self):
|
||||
return 0
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column == 1:
|
||||
return self.itemData[column]
|
||||
elif column == 2:
|
||||
return pretty_bytes(self.itemData[column])
|
||||
elif column == 0:
|
||||
return self.itemData[column]
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
|
||||
class TreeModel(QAbstractItemModel):
|
||||
column_names = ['Name', 'Modified', 'Size']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
|
||||
self.rootItem = FolderItem(path='', name='', modified=None)
|
||||
self.rootItem.load_children()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return 3
|
||||
class DiffTree(TreeModel):
|
||||
def __init__(
|
||||
self,
|
||||
files_with_attributes,
|
||||
nested_file_list,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
files_with_attributes, nested_file_list, parent=parent
|
||||
)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
|
@ -230,61 +131,7 @@ class TreeModel(QAbstractItemModel):
|
|||
else:
|
||||
return None
|
||||
|
||||
def canFetchMore(self, index):
|
||||
if not index.isValid():
|
||||
return False
|
||||
item = index.internalPointer()
|
||||
return not item.is_loaded
|
||||
|
||||
def fetchMore(self, index):
|
||||
item = index.internalPointer()
|
||||
item.load_children()
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags
|
||||
return Qt.ItemIsEnabled
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.column_names[section]
|
||||
|
||||
return None
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
childItem = parentItem.child(row)
|
||||
if childItem:
|
||||
return self.createIndex(row, column, childItem)
|
||||
else:
|
||||
return QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
childItem = index.internalPointer()
|
||||
parentItem = childItem.parent()
|
||||
|
||||
if parentItem == self.rootItem:
|
||||
return QModelIndex()
|
||||
|
||||
return self.createIndex(parentItem.row(), 0, parentItem)
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.column() > 0:
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
return parentItem.childCount()
|
||||
|
|
|
@ -4,50 +4,45 @@ import datetime
|
|||
from collections import namedtuple
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication, QHeaderView
|
||||
|
||||
from vorta.utils import get_asset, pretty_bytes, get_dict_from_list, nested_dict
|
||||
from vorta.utils import get_asset, get_dict_from_list, nested_dict
|
||||
from vorta.views.tree_view import TreeModel
|
||||
|
||||
uifile = get_asset('UI/extractdialog.ui')
|
||||
uifile = get_asset("UI/extractdialog.ui")
|
||||
ExtractDialogUI, ExtractDialogBase = uic.loadUiType(uifile)
|
||||
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
|
||||
|
||||
files_with_attributes = None
|
||||
nested_file_list = None
|
||||
selected_files_folders = None
|
||||
ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
|
||||
|
||||
|
||||
class ExtractDialog(ExtractDialogBase, ExtractDialogUI):
|
||||
def __init__(self, fs_data, archive):
|
||||
super().__init__()
|
||||
self.setupUi(self)
|
||||
global files_with_attributes, nested_file_list, selected_files_folders
|
||||
|
||||
# Clear global file lists
|
||||
files_with_attributes = []
|
||||
nested_file_list = nested_dict()
|
||||
selected_files_folders = set()
|
||||
self.selected = set()
|
||||
|
||||
def parse_line(line):
|
||||
size, modified, full_path = line.split('\t')
|
||||
size, modified, full_path = line.split("\t")
|
||||
size = int(size)
|
||||
dir, name = os.path.split(full_path)
|
||||
|
||||
# add to nested dict of folders to find nested dirs.
|
||||
d = get_dict_from_list(nested_file_list, dir.split('/'))
|
||||
d = get_dict_from_list(nested_file_list, dir.split("/"))
|
||||
if name not in d:
|
||||
d[name] = {}
|
||||
|
||||
return size, modified, name, dir
|
||||
|
||||
for l in fs_data.split('\n'):
|
||||
for l in fs_data.split("\n"):
|
||||
try:
|
||||
files_with_attributes.append(parse_line(l))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
model = TreeModel()
|
||||
model = ExtractTree(files_with_attributes, nested_file_list, self.selected)
|
||||
|
||||
view = self.treeView
|
||||
view.setAlternatingRowColors(True)
|
||||
|
@ -59,141 +54,22 @@ class ExtractDialog(ExtractDialogBase, ExtractDialogUI):
|
|||
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
|
||||
self.archiveNameLabel.setText(f'{archive.name}, {archive.time}')
|
||||
self.archiveNameLabel.setText(f"{archive.name}, {archive.time}")
|
||||
self.cancelButton.clicked.connect(self.close)
|
||||
self.extractButton.clicked.connect(self.accept)
|
||||
self.selected = selected_files_folders
|
||||
|
||||
|
||||
class FolderItem:
|
||||
def __init__(self, path, name, modified, parent=None):
|
||||
self.parentItem = parent
|
||||
self.path = path
|
||||
self.itemData = [name, modified]
|
||||
self.childItems = []
|
||||
self.checkedState = False
|
||||
|
||||
# Pre-filter children
|
||||
self._filtered_children = []
|
||||
search_path = os.path.join(self.path, name)
|
||||
if parent is None: # Find path for root folder
|
||||
for root_folder in nested_file_list.keys():
|
||||
self._filtered_children.append((0, '', root_folder, '', ))
|
||||
else:
|
||||
self.checkedState = parent.checkedState # If there is a parent, use its checked-status.
|
||||
|
||||
# This adds direct children.
|
||||
self._filtered_children = [f for f in files_with_attributes if search_path == f[3]]
|
||||
|
||||
# Add nested folders.
|
||||
for immediate_child in get_dict_from_list(nested_file_list, search_path.split('/')).keys():
|
||||
if not [True for child in self._filtered_children if child[2] == immediate_child]:
|
||||
self._filtered_children.append((0, '', immediate_child, search_path))
|
||||
|
||||
self.is_loaded = False
|
||||
|
||||
def load_children(self):
|
||||
for child_item in self._filtered_children:
|
||||
if child_item[0] > 0: # This is a file
|
||||
self.childItems.append(FileItem(
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
size=child_item[0],
|
||||
parent=self))
|
||||
else: # Folder
|
||||
self.childItems.append(
|
||||
FolderItem(
|
||||
path=child_item[3],
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
parent=self))
|
||||
|
||||
self.is_loaded = True
|
||||
|
||||
def setCheckedState(self, value):
|
||||
if value == 2:
|
||||
self.checkedState = True
|
||||
selected_files_folders.add(
|
||||
os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0]))
|
||||
else:
|
||||
self.checkedState = False
|
||||
path_to_remove = os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0])
|
||||
if path_to_remove in selected_files_folders:
|
||||
selected_files_folders.remove(path_to_remove)
|
||||
|
||||
if hasattr(self, 'childItems'):
|
||||
for child in self.childItems:
|
||||
child.setCheckedState(value)
|
||||
|
||||
def getCheckedState(self):
|
||||
if self.checkedState:
|
||||
return Qt.Checked
|
||||
else:
|
||||
return Qt.Unchecked
|
||||
|
||||
def child(self, row):
|
||||
return self.childItems[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self._filtered_children)
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column <= 1:
|
||||
return self.itemData[column]
|
||||
else:
|
||||
return None
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
if self.parentItem:
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class FileItem(FolderItem):
|
||||
def __init__(self, name, modified, size, parent=None):
|
||||
self.parentItem = parent
|
||||
self.itemData = [name, modified, size] # dt.strptime(modified, ISO_FORMAT)
|
||||
self.checkedState = parent.checkedState
|
||||
|
||||
def childCount(self):
|
||||
return 0
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column == 1:
|
||||
return self.itemData[column] # .strftime('%Y-%m-%dT%H:%M')
|
||||
elif column == 2:
|
||||
return pretty_bytes(self.itemData[column])
|
||||
elif column == 0:
|
||||
return self.itemData[column]
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
|
||||
class TreeModel(QAbstractItemModel):
|
||||
column_names = ['Name', 'Modified', 'Size']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
|
||||
self.rootItem = FolderItem(path='', name='', modified=None)
|
||||
self.rootItem.load_children()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return 3
|
||||
class ExtractTree(TreeModel):
|
||||
def __init__(
|
||||
self,
|
||||
files_with_attributes,
|
||||
nested_file_list,
|
||||
selected_files_folders,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
files_with_attributes, nested_file_list, selected_files_folders, parent
|
||||
)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
|
@ -208,86 +84,24 @@ class TreeModel(QAbstractItemModel):
|
|||
else:
|
||||
return None
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if role == Qt.CheckStateRole:
|
||||
item = index.internalPointer()
|
||||
item.setCheckedState(value)
|
||||
self.dataChanged.emit(QModelIndex(), QModelIndex(), [])
|
||||
|
||||
return True
|
||||
|
||||
def canFetchMore(self, index):
|
||||
if not index.isValid():
|
||||
return False
|
||||
item = index.internalPointer()
|
||||
return not item.is_loaded
|
||||
|
||||
def fetchMore(self, index):
|
||||
item = index.internalPointer()
|
||||
item.load_children()
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags
|
||||
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.column_names[section]
|
||||
|
||||
return None
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
childItem = parentItem.child(row)
|
||||
if childItem:
|
||||
return self.createIndex(row, column, childItem)
|
||||
else:
|
||||
return QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
childItem = index.internalPointer()
|
||||
parentItem = childItem.parent()
|
||||
|
||||
if parentItem == self.rootItem:
|
||||
return QModelIndex()
|
||||
|
||||
return self.createIndex(parentItem.row(), 0, parentItem)
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.column() > 0:
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
return parentItem.childCount()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
For local testing:
|
||||
|
||||
borg list --progress --info --log-json --format="{size:8d}{TAB}{mtime}{TAB}{path}{NL}"
|
||||
"""
|
||||
FakeArchive = namedtuple('Archive', ['name', 'time'])
|
||||
FakeArchive = namedtuple("Archive", ["name", "time"])
|
||||
app = QApplication(sys.argv)
|
||||
test_list = open('/Users/manu/Downloads/nyx2-list.txt').read()
|
||||
test_list = open("/Users/manu/Downloads/nyx2-list.txt").read()
|
||||
|
||||
archive = FakeArchive('test-archive', datetime.datetime.now())
|
||||
archive = FakeArchive("test-archive", datetime.datetime.now())
|
||||
view = ExtractDialog(test_list, archive)
|
||||
view.show()
|
||||
sys.exit(app.exec_())
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
|
||||
|
||||
import os
|
||||
import abc
|
||||
|
||||
from vorta.utils import get_dict_from_list, pretty_bytes
|
||||
|
||||
|
||||
class FolderItem:
|
||||
def __init__(
|
||||
self,
|
||||
path,
|
||||
name,
|
||||
modified,
|
||||
files_with_attributes,
|
||||
nested_file_list,
|
||||
selected_files_folders=None,
|
||||
parent=None,
|
||||
):
|
||||
self.parentItem = parent
|
||||
self.path = path
|
||||
self.itemData = [name, modified]
|
||||
self.childItems = []
|
||||
self.checkedState = False
|
||||
self.files_with_attributes = files_with_attributes
|
||||
self.nested_file_list = nested_file_list
|
||||
self.selected_files_folders = selected_files_folders
|
||||
|
||||
# Pre-filter children
|
||||
self._filtered_children = []
|
||||
search_path = os.path.join(self.path, name)
|
||||
if parent is None: # Find path for root folder
|
||||
for root_folder in nested_file_list.keys():
|
||||
self._filtered_children.append((0, "", root_folder, "",))
|
||||
else:
|
||||
self.checkedState = (
|
||||
parent.checkedState
|
||||
) # If there is a parent, use its checked-status.
|
||||
|
||||
# This adds direct children.
|
||||
self._filtered_children = [
|
||||
f for f in files_with_attributes if search_path == f[3]
|
||||
]
|
||||
|
||||
# Add nested folders.
|
||||
for immediate_child in get_dict_from_list(
|
||||
nested_file_list, search_path.split("/")
|
||||
).keys():
|
||||
if not [
|
||||
True
|
||||
for child in self._filtered_children
|
||||
if child[2] == immediate_child
|
||||
]:
|
||||
self._filtered_children.append(
|
||||
(0, "", immediate_child, search_path)
|
||||
)
|
||||
|
||||
self.is_loaded = False
|
||||
|
||||
def load_children(self):
|
||||
for child_item in self._filtered_children:
|
||||
if child_item[0] > 0: # This is a file
|
||||
self.childItems.append(
|
||||
FileItem(
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
size=child_item[0],
|
||||
files_with_attributes=self.files_with_attributes,
|
||||
nested_file_list=self.nested_file_list,
|
||||
selected_files_folders=self.selected_files_folders,
|
||||
parent=self,
|
||||
)
|
||||
)
|
||||
else: # Folder
|
||||
self.childItems.append(
|
||||
FolderItem(
|
||||
path=child_item[3],
|
||||
name=child_item[2],
|
||||
modified=child_item[1],
|
||||
files_with_attributes=self.files_with_attributes,
|
||||
nested_file_list=self.nested_file_list,
|
||||
selected_files_folders=self.selected_files_folders,
|
||||
parent=self,
|
||||
)
|
||||
)
|
||||
self.is_loaded = True
|
||||
|
||||
def setCheckedState(self, value):
|
||||
if value == 2:
|
||||
self.checkedState = True
|
||||
self.selected_files_folders.add(
|
||||
os.path.join(
|
||||
self.parentItem.path, self.parentItem.data(0), self.itemData[0]
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.checkedState = False
|
||||
path_to_remove = os.path.join(
|
||||
self.parentItem.path, self.parentItem.data(0), self.itemData[0]
|
||||
)
|
||||
if path_to_remove in self.selected_files_folders:
|
||||
self.selected_files_folders.remove(path_to_remove)
|
||||
|
||||
if hasattr(self, "childItems"):
|
||||
for child in self.childItems:
|
||||
child.setCheckedState(value)
|
||||
|
||||
def getCheckedState(self):
|
||||
if self.checkedState:
|
||||
return Qt.Checked
|
||||
else:
|
||||
return Qt.Unchecked
|
||||
|
||||
def child(self, row):
|
||||
return self.childItems[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self._filtered_children)
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column <= 1:
|
||||
return self.itemData[column]
|
||||
else:
|
||||
return None
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
if self.parentItem:
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class FileItem(FolderItem):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
modified,
|
||||
size,
|
||||
files_with_attributes,
|
||||
nested_file_list,
|
||||
selected_files_folders=None,
|
||||
parent=None,
|
||||
):
|
||||
self.parentItem = parent
|
||||
self.itemData = [name, modified, size]
|
||||
self.checkedState = parent.checkedState
|
||||
self.files_with_attributes = files_with_attributes
|
||||
self.nested_file_list = nested_file_list
|
||||
self.selected_files_folders = selected_files_folders
|
||||
|
||||
def childCount(self):
|
||||
return 0
|
||||
|
||||
def columnCount(self):
|
||||
return 3
|
||||
|
||||
def data(self, column):
|
||||
if column == 1:
|
||||
return self.itemData[column]
|
||||
elif column == 2:
|
||||
return pretty_bytes(self.itemData[column])
|
||||
elif column == 0:
|
||||
return self.itemData[column]
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
return self.parentItem.childItems.index(self)
|
||||
|
||||
|
||||
class TreeModel(QAbstractItemModel):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
column_names = ["Name", "Modified", "Size"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files_with_attributes,
|
||||
nested_file_list,
|
||||
selected_files_folders=None,
|
||||
parent=None,
|
||||
):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
|
||||
self.rootItem = FolderItem(
|
||||
path="",
|
||||
name="",
|
||||
files_with_attributes=files_with_attributes,
|
||||
nested_file_list=nested_file_list,
|
||||
selected_files_folders=selected_files_folders,
|
||||
modified=None,
|
||||
)
|
||||
self.rootItem.load_children()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return 3
|
||||
|
||||
@abc.abstractmethod
|
||||
def data(self, index, role):
|
||||
return
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if role == Qt.CheckStateRole:
|
||||
item = index.internalPointer()
|
||||
item.setCheckedState(value)
|
||||
self.dataChanged.emit(QModelIndex(), QModelIndex(), [])
|
||||
|
||||
return True
|
||||
|
||||
def canFetchMore(self, index):
|
||||
if not index.isValid():
|
||||
return False
|
||||
item = index.internalPointer()
|
||||
return not item.is_loaded
|
||||
|
||||
def fetchMore(self, index):
|
||||
item = index.internalPointer()
|
||||
item.load_children()
|
||||
|
||||
@abc.abstractmethod
|
||||
def flags(self, index):
|
||||
return
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.column_names[section]
|
||||
|
||||
return None
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
childItem = parentItem.child(row)
|
||||
if childItem:
|
||||
return self.createIndex(row, column, childItem)
|
||||
else:
|
||||
return QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
childItem = index.internalPointer()
|
||||
parentItem = childItem.parent()
|
||||
|
||||
if parentItem == self.rootItem:
|
||||
return QModelIndex()
|
||||
|
||||
return self.createIndex(parentItem.row(), 0, parentItem)
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.column() > 0:
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
return parentItem.childCount()
|
|
@ -0,0 +1,2 @@
|
|||
+7 B 0 B [-rw-rw-r-- -> -rw-rw-rw-] test/hallo
|
||||
added 0 B test/tschüss
|
|
@ -32,6 +32,9 @@ def init_db(qapp):
|
|||
test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1)
|
||||
test_archive.save()
|
||||
|
||||
test_archive1 = ArchiveModel(snapshot_id='99998', name='test-archive1', time=dt(2000, 1, 1, 0, 0), repo=1)
|
||||
test_archive1.save()
|
||||
|
||||
source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo)
|
||||
source_dir.save()
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ def test_archive_extract(qapp, qtbot, mocker, borg_json_output, monkeypatch):
|
|||
main.tabWidget.setCurrentIndex(3)
|
||||
|
||||
tab.populate_from_profile()
|
||||
qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 1)
|
||||
qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2)
|
||||
|
||||
monkeypatch.setattr(
|
||||
vorta.views.extract_dialog.ExtractDialog, "exec_", lambda *args: True
|
||||
|
@ -131,6 +131,40 @@ def test_archive_extract(qapp, qtbot, mocker, borg_json_output, monkeypatch):
|
|||
|
||||
assert tab._window.treeView.model().rootItem.childItems[0].data(0) == 'Users'
|
||||
tab._window.treeView.model().rootItem.childItems[0].load_children()
|
||||
|
||||
assert tab._window.archiveNameLabel.text().startswith('test-archive, 2000')
|
||||
tab._window.accept()
|
||||
|
||||
|
||||
def test_archive_diff(qapp, qtbot, mocker, borg_json_output, monkeypatch):
|
||||
main = qapp.main_window
|
||||
tab = main.archiveTab
|
||||
main.tabWidget.setCurrentIndex(3)
|
||||
|
||||
tab.populate_from_profile()
|
||||
qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2)
|
||||
|
||||
monkeypatch.setattr(
|
||||
vorta.views.diff_dialog.DiffDialog, "exec_", lambda *args: True
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
tab, "selected_archives", (0, 1)
|
||||
)
|
||||
|
||||
stdout, stderr = borg_json_output('diff_archives')
|
||||
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
|
||||
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
|
||||
|
||||
qtbot.mouseClick(tab.diffButton, QtCore.Qt.LeftButton)
|
||||
qtbot.waitUntil(lambda: hasattr(tab, '_window'), timeout=5000)
|
||||
|
||||
monkeypatch.setattr(
|
||||
vorta.views.diff_result.DiffResult, "exec_", lambda *args: True
|
||||
)
|
||||
qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), timeout=5000)
|
||||
|
||||
assert tab._resultwindow.treeView.model().rootItem.childItems[0].data(0) == 'test'
|
||||
tab._resultwindow.treeView.model().rootItem.childItems[0].load_children()
|
||||
|
||||
assert tab._resultwindow.archiveNameLabel_1.text() == 'test-archive'
|
||||
tab._resultwindow.accept()
|
||||
|
|
|
@ -101,8 +101,8 @@ def test_create(qapp, borg_json_output, mocker, qtbot):
|
|||
qtbot.waitUntil(lambda: main.createProgressText.text().startswith('Backup finished.'), timeout=3000)
|
||||
qtbot.waitUntil(lambda: main.createStartBtn.isEnabled(), timeout=3000)
|
||||
assert EventLogModel.select().count() == 1
|
||||
assert ArchiveModel.select().count() == 2
|
||||
assert ArchiveModel.select().count() == 3
|
||||
assert RepoModel.get(id=1).unique_size == 15520474
|
||||
assert main.createStartBtn.isEnabled()
|
||||
assert main.archiveTab.archiveTable.rowCount() == 2
|
||||
assert main.archiveTab.archiveTable.rowCount() == 3
|
||||
assert main.scheduleTab.logTableWidget.rowCount() == 1
|
||||
|
|
Loading…
Reference in New Issue