2018-11-22 00:43:37 +00:00
|
|
|
from PyQt5 import uic
|
2020-10-30 08:30:52 +00:00
|
|
|
from ..models import SourceFileModel, BackupProfileMixin, SettingsModel
|
|
|
|
from ..utils import get_asset, choose_file_dialog, pretty_bytes, sort_sizes, FilePathInfoAsync
|
|
|
|
from PyQt5 import QtCore
|
|
|
|
from PyQt5.QtCore import QFileInfo
|
|
|
|
from PyQt5.QtWidgets import QApplication, QMessageBox, QTableWidgetItem, QHeaderView
|
2020-06-22 07:59:28 +00:00
|
|
|
import os
|
2018-10-27 17:24:34 +00:00
|
|
|
|
2018-10-29 05:49:25 +00:00
|
|
|
uifile = get_asset('UI/sourcetab.ui')
|
2018-10-27 17:24:34 +00:00
|
|
|
SourceUI, SourceBase = uic.loadUiType(uifile)
|
|
|
|
|
|
|
|
|
2020-10-30 08:30:52 +00:00
|
|
|
class SourceColumn:
|
|
|
|
Path = 0
|
|
|
|
Type = 1
|
|
|
|
Size = 2
|
|
|
|
FilesCount = 3
|
|
|
|
|
|
|
|
|
|
|
|
class SizeItem(QTableWidgetItem):
|
|
|
|
def __lt__(self, other):
|
|
|
|
return sort_sizes([self.text(), other.text()]) == [self.text(), other.text()]
|
|
|
|
|
|
|
|
|
|
|
|
class FilesCount(QTableWidgetItem):
|
|
|
|
def __lt__(self, other):
|
|
|
|
# Verify that conversion is only performed on valid integers
|
|
|
|
# If one of the 2 elements is no number, put these elements at the end
|
|
|
|
# This is important if the text is "Calculating..." or ""
|
|
|
|
if self.text().isdigit() and other.text().isdigit():
|
|
|
|
return int(self.text()) < int(other.text()) # Compare & return result
|
|
|
|
else:
|
|
|
|
if not self.text().isdigit():
|
|
|
|
return 1 # Move one down if current item has no valid count
|
|
|
|
if not other.text().isdigit():
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
2018-10-31 16:09:01 +00:00
|
|
|
class SourceTab(SourceBase, SourceUI, BackupProfileMixin):
|
2020-10-30 08:30:52 +00:00
|
|
|
updateThreads = []
|
|
|
|
|
2018-10-27 17:24:34 +00:00
|
|
|
def __init__(self, parent=None):
|
|
|
|
super().__init__(parent)
|
|
|
|
self.setupUi(parent)
|
|
|
|
|
2020-10-30 08:30:52 +00:00
|
|
|
header = self.sourceFilesWidget.horizontalHeader()
|
|
|
|
|
|
|
|
header.setVisible(True)
|
|
|
|
header.setSortIndicatorShown(1)
|
|
|
|
|
|
|
|
header.setSectionResizeMode(SourceColumn.Path, QHeaderView.Stretch)
|
|
|
|
header.setSectionResizeMode(SourceColumn.Type, QHeaderView.ResizeToContents)
|
|
|
|
header.setSectionResizeMode(SourceColumn.Size, QHeaderView.ResizeToContents)
|
|
|
|
header.setSectionResizeMode(SourceColumn.FilesCount, QHeaderView.ResizeToContents)
|
|
|
|
|
|
|
|
self.sourceFilesWidget.setSortingEnabled(True)
|
2018-11-24 04:12:12 +00:00
|
|
|
self.sourceAddFolder.clicked.connect(lambda: self.source_add(want_folder=True))
|
|
|
|
self.sourceAddFile.clicked.connect(lambda: self.source_add(want_folder=False))
|
2018-10-27 17:24:34 +00:00
|
|
|
self.sourceRemove.clicked.connect(self.source_remove)
|
2020-10-30 08:30:52 +00:00
|
|
|
self.sourcesUpdate.clicked.connect(self.sources_update)
|
2020-06-22 07:59:28 +00:00
|
|
|
self.paste.clicked.connect(self.paste_text)
|
2018-11-17 08:51:53 +00:00
|
|
|
self.excludePatternsField.textChanged.connect(self.save_exclude_patterns)
|
|
|
|
self.excludeIfPresentField.textChanged.connect(self.save_exclude_if_present)
|
|
|
|
self.populate_from_profile()
|
2018-10-27 17:24:34 +00:00
|
|
|
|
2020-10-30 08:30:52 +00:00
|
|
|
def set_path_info(self, path, data_size, files_count):
|
|
|
|
items = self.sourceFilesWidget.findItems(path, QtCore.Qt.MatchExactly)
|
|
|
|
# Conversion int->str->int needed because QT limits int to 32-bit
|
|
|
|
data_size = int(data_size)
|
|
|
|
files_count = int(files_count)
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
db_item = SourceFileModel.get(dir=path)
|
|
|
|
if QFileInfo(path).isDir():
|
|
|
|
self.sourceFilesWidget.item(item.row(), SourceColumn.Type).setText(self.tr("Folder"))
|
|
|
|
self.sourceFilesWidget.item(item.row(), SourceColumn.FilesCount).setText(format(files_count))
|
|
|
|
db_item.path_isdir = True
|
|
|
|
else:
|
|
|
|
self.sourceFilesWidget.item(item.row(), SourceColumn.Type).setText(self.tr("File"))
|
|
|
|
# No files count, if entry itself is a file
|
|
|
|
self.sourceFilesWidget.item(item.row(), SourceColumn.FilesCount).setText("")
|
|
|
|
db_item.path_isdir = False
|
|
|
|
self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes(data_size))
|
|
|
|
|
|
|
|
db_item.dir_size = data_size
|
|
|
|
db_item.dir_files_count = files_count
|
|
|
|
db_item.save()
|
|
|
|
# Remove thread from list when it's done
|
|
|
|
for thrd in self.updateThreads:
|
|
|
|
if thrd.objectName() == path:
|
|
|
|
self.updateThreads.remove(thrd)
|
|
|
|
|
|
|
|
def update_path_info(self, index_row):
|
|
|
|
path = self.sourceFilesWidget.item(index_row, SourceColumn.Path).text()
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.Type).setText(self.tr("Calculating..."))
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(self.tr("Calculating..."))
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.FilesCount).setText(self.tr("Calculating..."))
|
|
|
|
getDir = FilePathInfoAsync(path)
|
|
|
|
getDir.signal.connect(self.set_path_info)
|
|
|
|
getDir.setObjectName(path)
|
|
|
|
self.updateThreads.append(getDir) # this is ugly, is there a better way to keep the thread object?
|
|
|
|
getDir.start()
|
|
|
|
|
|
|
|
def add_source_to_table(self, source, update_data):
|
|
|
|
index_row = self.sourceFilesWidget.rowCount()
|
|
|
|
self.sourceFilesWidget.insertRow(index_row)
|
|
|
|
# Insert all items on current row
|
|
|
|
self.sourceFilesWidget.setItem(index_row, SourceColumn.Path, QTableWidgetItem(source.dir))
|
|
|
|
self.sourceFilesWidget.setItem(index_row, SourceColumn.Type, QTableWidgetItem(""))
|
|
|
|
self.sourceFilesWidget.setItem(index_row, SourceColumn.Size, SizeItem(""))
|
|
|
|
self.sourceFilesWidget.setItem(index_row, SourceColumn.FilesCount, FilesCount(""))
|
|
|
|
|
|
|
|
if update_data:
|
|
|
|
self.update_path_info(index_row)
|
|
|
|
else: # Use cached data from DB
|
|
|
|
if source.dir_size > -1:
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(pretty_bytes(source.dir_size))
|
|
|
|
|
|
|
|
if source.path_isdir:
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.Type).setText(self.tr("Folder"))
|
|
|
|
self.sourceFilesWidget.item(index_row,
|
|
|
|
SourceColumn.FilesCount).setText(format(source.dir_files_count))
|
|
|
|
else:
|
|
|
|
self.sourceFilesWidget.item(index_row, SourceColumn.Type).setText(self.tr("File"))
|
|
|
|
|
2018-11-17 08:51:53 +00:00
|
|
|
def populate_from_profile(self):
|
|
|
|
profile = self.profile()
|
|
|
|
self.excludePatternsField.textChanged.disconnect()
|
|
|
|
self.excludeIfPresentField.textChanged.disconnect()
|
2020-10-30 08:30:52 +00:00
|
|
|
self.sourceFilesWidget.setRowCount(0) # Clear rows
|
2018-11-17 08:51:53 +00:00
|
|
|
self.excludePatternsField.clear()
|
|
|
|
self.excludeIfPresentField.clear()
|
|
|
|
|
2018-12-05 09:05:47 +00:00
|
|
|
for source in SourceFileModel.select().where(SourceFileModel.profile == profile):
|
2020-10-30 08:30:52 +00:00
|
|
|
self.add_source_to_table(source, False)
|
2018-10-28 09:35:25 +00:00
|
|
|
|
2020-10-30 08:30:52 +00:00
|
|
|
# Initially, sort entries by path name in ascending order
|
|
|
|
self.sourceFilesWidget.model().sort(SourceColumn.Path, QtCore.Qt.AscendingOrder)
|
2018-11-17 08:51:53 +00:00
|
|
|
self.excludePatternsField.appendPlainText(profile.exclude_patterns)
|
|
|
|
self.excludeIfPresentField.appendPlainText(profile.exclude_if_present)
|
2018-10-28 09:35:25 +00:00
|
|
|
self.excludePatternsField.textChanged.connect(self.save_exclude_patterns)
|
|
|
|
self.excludeIfPresentField.textChanged.connect(self.save_exclude_if_present)
|
|
|
|
|
2020-10-30 08:30:52 +00:00
|
|
|
def sources_update(self):
|
|
|
|
for row in range(0, self.sourceFilesWidget.rowCount()):
|
|
|
|
self.update_path_info(row) # Update data for each entry
|
|
|
|
|
2018-11-24 04:12:12 +00:00
|
|
|
def source_add(self, want_folder):
|
2018-11-20 00:50:52 +00:00
|
|
|
def receive():
|
2020-08-30 06:04:40 +00:00
|
|
|
dirs = dialog.selectedFiles()
|
|
|
|
for dir in dirs:
|
2020-10-30 08:30:52 +00:00
|
|
|
new_source, created = SourceFileModel.get_or_create(dir=dir,
|
|
|
|
dir_size=-1,
|
|
|
|
dir_files_count=-1,
|
|
|
|
path_isdir=False,
|
|
|
|
profile=self.profile())
|
2018-11-20 00:50:52 +00:00
|
|
|
if created:
|
2020-10-30 08:30:52 +00:00
|
|
|
self.add_source_to_table(new_source, SettingsModel.get(key="get_srcpath_datasize").value)
|
2018-11-20 00:50:52 +00:00
|
|
|
new_source.save()
|
|
|
|
|
2020-08-30 06:04:40 +00:00
|
|
|
msg = self.tr("Choose directory to back up") if want_folder else self.tr("Choose file(s) to back up")
|
2019-01-20 03:50:10 +00:00
|
|
|
dialog = choose_file_dialog(self, msg, want_folder=want_folder)
|
2018-11-20 00:50:52 +00:00
|
|
|
dialog.open(receive)
|
|
|
|
|
2018-10-27 17:24:34 +00:00
|
|
|
def source_remove(self):
|
2020-10-30 08:30:52 +00:00
|
|
|
indexes = self.sourceFilesWidget.selectionModel().selectedRows()
|
2020-08-24 02:05:07 +00:00
|
|
|
# sort indexes, starting with lowest
|
|
|
|
indexes.sort()
|
2020-10-30 08:30:52 +00:00
|
|
|
# remove each selected row, starting with highest index (otherways, higher indexes become invalid)
|
2020-08-24 02:05:07 +00:00
|
|
|
for index in reversed(indexes):
|
2020-10-30 08:30:52 +00:00
|
|
|
db_item = SourceFileModel.get(dir=self.sourceFilesWidget.item(index.row(), SourceColumn.Path).text())
|
2019-04-16 01:51:25 +00:00
|
|
|
db_item.delete_instance()
|
2020-10-30 08:30:52 +00:00
|
|
|
self.sourceFilesWidget.removeRow(index.row())
|
2018-10-28 09:35:25 +00:00
|
|
|
|
|
|
|
def save_exclude_patterns(self):
|
2018-11-04 08:23:17 +00:00
|
|
|
profile = self.profile()
|
2018-10-31 16:09:01 +00:00
|
|
|
profile.exclude_patterns = self.excludePatternsField.toPlainText()
|
|
|
|
profile.save()
|
2018-10-28 09:35:25 +00:00
|
|
|
|
|
|
|
def save_exclude_if_present(self):
|
2018-11-04 08:23:17 +00:00
|
|
|
profile = self.profile()
|
2018-10-31 16:09:01 +00:00
|
|
|
profile.exclude_if_present = self.excludeIfPresentField.toPlainText()
|
|
|
|
profile.save()
|
2020-06-22 07:59:28 +00:00
|
|
|
|
|
|
|
def paste_text(self):
|
|
|
|
sources = QApplication.clipboard().text().splitlines()
|
|
|
|
invalidSources = ""
|
|
|
|
for source in sources:
|
|
|
|
if len(source) > 0: # Ignore empty newlines
|
|
|
|
if not os.path.exists(source):
|
|
|
|
invalidSources = invalidSources + "\n" + source
|
|
|
|
else:
|
|
|
|
new_source, created = SourceFileModel.get_or_create(dir=source, profile=self.profile())
|
|
|
|
if created:
|
|
|
|
self.sourceFilesWidget.addItem(source)
|
|
|
|
new_source.save()
|
|
|
|
|
|
|
|
if len(invalidSources) != 0: # Check if any invalid paths
|
|
|
|
msg = QMessageBox()
|
|
|
|
msg.setText("Some of your sources are invalid:" + invalidSources)
|
|
|
|
msg.exec()
|