mirror of https://github.com/borgbase/vorta
parent
6e0904647b
commit
ab368b32a4
|
@ -92,6 +92,11 @@ font-weight: bold;
|
|||
<string>Duration</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Mounted</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import os.path
|
||||
import psutil
|
||||
from .borg_thread import BorgThread
|
||||
from ..i18n import trans_late
|
||||
|
@ -20,7 +21,7 @@ class BorgUmountThread(BorgThread):
|
|||
partitions = psutil.disk_partitions(all=True)
|
||||
for p in partitions:
|
||||
if p.device == 'borgfs':
|
||||
ret['active_mount_points'].append(p.mountpoint)
|
||||
ret['active_mount_points'].append(os.path.normpath(p.mountpoint))
|
||||
|
||||
if len(ret['active_mount_points']) == 0:
|
||||
ret['message'] = trans_late('messages', 'No active Borg mounts found.')
|
||||
|
|
|
@ -10,6 +10,7 @@ import getpass
|
|||
from collections import defaultdict
|
||||
from functools import reduce
|
||||
import operator
|
||||
import psutil
|
||||
|
||||
from paramiko.rsakey import RSAKey
|
||||
from paramiko.ecdsakey import ECDSAKey
|
||||
|
@ -247,3 +248,24 @@ def format_archive_name(profile, archive_name_tpl):
|
|||
'user': getpass.getuser()
|
||||
}
|
||||
return archive_name_tpl.format(**available_vars)
|
||||
|
||||
|
||||
def get_mount_points(repo_url):
|
||||
mount_points = {}
|
||||
for proc in psutil.process_iter():
|
||||
if proc.name() == 'borg' or proc.name().startswith('python'):
|
||||
if 'mount' not in proc.cmdline():
|
||||
continue
|
||||
|
||||
for idx, parameter in enumerate(proc.cmdline()):
|
||||
if parameter.startswith(repo_url + '::'):
|
||||
archive_name = parameter[len(repo_url) + 2:]
|
||||
|
||||
# The borg mount command specifies that the mount_point
|
||||
# parameter comes after the archive name
|
||||
if len(proc.cmdline()) > idx + 1:
|
||||
mount_point = proc.cmdline()[idx + 1]
|
||||
mount_points[archive_name] = mount_point
|
||||
break
|
||||
|
||||
return mount_points
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import os.path
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from PyQt5 import uic, QtCore
|
||||
from PyQt5.QtGui import QIcon, QDesktopServices
|
||||
from PyQt5.QtWidgets import QTableWidgetItem, QTableView, QHeaderView
|
||||
|
||||
from vorta.borg.prune import BorgPruneThread
|
||||
|
@ -12,7 +14,7 @@ from vorta.borg.extract import BorgExtractThread
|
|||
from vorta.borg.umount import BorgUmountThread
|
||||
from vorta.views.extract_dialog import ExtractDialog
|
||||
from vorta.i18n import translate
|
||||
from vorta.utils import get_asset, pretty_bytes, choose_file_dialog, format_archive_name
|
||||
from vorta.utils import get_asset, pretty_bytes, choose_file_dialog, format_archive_name, get_mount_points
|
||||
from vorta.models import BackupProfileMixin, ArchiveModel
|
||||
|
||||
uifile = get_asset('UI/archivetab.ui')
|
||||
|
@ -25,23 +27,29 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(parent)
|
||||
self.mount_point = None
|
||||
self.mount_points = {}
|
||||
self.menu = None
|
||||
self.toolBox.setCurrentIndex(0)
|
||||
|
||||
self.folder_icon = QIcon(':/icons/folder-open.svg')
|
||||
header = self.archiveTable.horizontalHeader()
|
||||
header.setVisible(True)
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(4, QHeaderView.Stretch)
|
||||
header.setStretchLastSection(True)
|
||||
|
||||
if sys.platform != 'darwin':
|
||||
self._set_status('') # Set platform-specific hints.
|
||||
|
||||
self.archiveTable.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.archiveTable.setSelectionMode(QTableView.SingleSelection)
|
||||
self.archiveTable.setEditTriggers(QTableView.NoEditTriggers)
|
||||
self.archiveTable.setAlternatingRowColors(True)
|
||||
self.archiveTable.cellDoubleClicked.connect(self.cell_double_clicked)
|
||||
self.archiveTable.itemSelectionChanged.connect(self.update_mount_button_text)
|
||||
|
||||
# Populate pruning options from database
|
||||
profile = self.profile()
|
||||
|
@ -75,9 +83,9 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
|
||||
def populate_from_profile(self):
|
||||
"""Populate archive list and prune settings from profile."""
|
||||
|
||||
profile = self.profile()
|
||||
if profile.repo is not None:
|
||||
self.mount_points = get_mount_points(profile.repo.url)
|
||||
self.toolBox.setItemText(0, self.tr('Archives for %s') % profile.repo.url)
|
||||
archives = [s for s in profile.repo.archives.select().order_by(ArchiveModel.time.desc())]
|
||||
|
||||
|
@ -91,13 +99,22 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
formatted_duration = str(timedelta(seconds=round(archive.duration)))
|
||||
else:
|
||||
formatted_duration = ''
|
||||
|
||||
self.archiveTable.setItem(row, 2, QTableWidgetItem(formatted_duration))
|
||||
self.archiveTable.setItem(row, 3, QTableWidgetItem(archive.name))
|
||||
|
||||
mount_point = self.mount_points.get(archive.name)
|
||||
if mount_point is not None:
|
||||
item = QTableWidgetItem(f'…{mount_point[-5:]}')
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
self.archiveTable.setItem(row, 4, QTableWidgetItem(archive.name))
|
||||
|
||||
self.archiveTable.setRowCount(len(archives))
|
||||
item = self.archiveTable.item(0, 0)
|
||||
self.archiveTable.scrollToItem(item)
|
||||
self._toggle_all_buttons(enabled=True)
|
||||
else:
|
||||
self.mount_points = {}
|
||||
self.archiveTable.setRowCount(0)
|
||||
self.toolBox.setItemText(0, self.tr('Archives'))
|
||||
self._toggle_all_buttons(enabled=False)
|
||||
|
@ -128,7 +145,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
# Conditions are met (borg binary available, etc)
|
||||
row_selected = self.archiveTable.selectionModel().selectedRows()
|
||||
if row_selected:
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 3)
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 4)
|
||||
if archive_cell:
|
||||
archive_name = archive_cell.text()
|
||||
params['cmd'][-1] += f'::{archive_name}'
|
||||
|
@ -174,6 +191,20 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
self._set_status(self.tr('Refreshed archives.'))
|
||||
self.populate_from_profile()
|
||||
|
||||
def selected_archive_name(self):
|
||||
row_selected = self.archiveTable.selectionModel().selectedRows()
|
||||
if row_selected:
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 4)
|
||||
if archive_cell:
|
||||
return archive_cell.text()
|
||||
return None
|
||||
|
||||
def set_mount_button_mode(self, mode):
|
||||
self.mountButton.clicked.disconnect()
|
||||
mount = (mode == 'Mount')
|
||||
self.mountButton.setText('Mount' if mount else 'Unmount')
|
||||
self.mountButton.clicked.connect(self.mount_action if mount else self.umount_action)
|
||||
|
||||
def mount_action(self):
|
||||
profile = self.profile()
|
||||
params = BorgMountThread.prepare(profile)
|
||||
|
@ -182,18 +213,16 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
return
|
||||
|
||||
# Conditions are met (borg binary available, etc)
|
||||
row_selected = self.archiveTable.selectionModel().selectedRows()
|
||||
if row_selected:
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 3)
|
||||
if archive_cell:
|
||||
archive_name = archive_cell.text()
|
||||
params['cmd'][-1] += f'::{archive_name}'
|
||||
archive_name = self.selected_archive_name()
|
||||
if archive_name:
|
||||
params['cmd'][-1] += f'::{archive_name}'
|
||||
params['current_archive'] = archive_name
|
||||
|
||||
def receive():
|
||||
mount_point = dialog.selectedFiles()
|
||||
if mount_point:
|
||||
params['cmd'].append(mount_point[0])
|
||||
self.mount_point = mount_point[0]
|
||||
self.mount_points[params['current_archive']] = mount_point[0]
|
||||
if params['ok']:
|
||||
self._toggle_all_buttons(False)
|
||||
thread = BorgMountThread(params['cmd'], params, parent=self)
|
||||
|
@ -201,45 +230,54 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
thread.result.connect(self.mount_result)
|
||||
thread.start()
|
||||
|
||||
dialog = choose_file_dialog(self, self.tr("Choose Mount Point"))
|
||||
dialog = choose_file_dialog(self, self.tr("Choose Mount Point"), want_folder=True)
|
||||
dialog.open(receive)
|
||||
|
||||
def mount_result(self, result):
|
||||
self._toggle_all_buttons(True)
|
||||
if result['returncode'] == 0:
|
||||
self._set_status(self.tr('Mounted successfully.'))
|
||||
self.mountButton.setText(self.tr('Unmount'))
|
||||
self.mountButton.clicked.disconnect()
|
||||
self.mountButton.clicked.connect(self.umount_action)
|
||||
else:
|
||||
self.mount_point = None
|
||||
self.update_mount_button_text()
|
||||
archive_name = result['params']['current_archive']
|
||||
row = self.row_of_archive(archive_name)
|
||||
item = QTableWidgetItem(f"…{result['cmd'][-1][-5:]}")
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
def umount_action(self):
|
||||
if self.mount_point is not None:
|
||||
archive_name = self.selected_archive_name()
|
||||
|
||||
mount_point = self.mount_points.get(archive_name)
|
||||
|
||||
if mount_point is not None:
|
||||
profile = self.profile()
|
||||
params = BorgUmountThread.prepare(profile)
|
||||
if not params['ok']:
|
||||
self._set_status(translate(params['message']))
|
||||
return
|
||||
|
||||
if self.mount_point in params['active_mount_points']:
|
||||
params['cmd'].append(self.mount_point)
|
||||
params['current_archive'] = archive_name
|
||||
|
||||
if os.path.normpath(mount_point) in params['active_mount_points']:
|
||||
params['cmd'].append(mount_point)
|
||||
thread = BorgUmountThread(params['cmd'], params, parent=self)
|
||||
thread.updated.connect(self.mountErrors.setText)
|
||||
thread.result.connect(self.umount_result)
|
||||
thread.start()
|
||||
else:
|
||||
self._set_status(self.tr('Mount point not active. Try restarting Vorta.'))
|
||||
self._set_status(self.tr('Mount point not active.'))
|
||||
return
|
||||
|
||||
def umount_result(self, result):
|
||||
self._toggle_all_buttons(True)
|
||||
if result['returncode'] == 0:
|
||||
self._set_status(self.tr('Un-mounted successfully.'))
|
||||
self.mountButton.setText(self.tr('Mount'))
|
||||
self.mountButton.clicked.disconnect()
|
||||
self.mountButton.clicked.connect(self.mount_action)
|
||||
self.mount_point = None
|
||||
self._set_status('Un-mounted successfully.')
|
||||
archive_name = result['params']['current_archive']
|
||||
del self.mount_points[archive_name]
|
||||
self.update_mount_button_text()
|
||||
row = self.row_of_archive(archive_name)
|
||||
item = QTableWidgetItem('')
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
def save_prune_setting(self, new_value=None):
|
||||
profile = self.profile()
|
||||
|
@ -253,7 +291,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
|
||||
row_selected = self.archiveTable.selectionModel().selectedRows()
|
||||
if row_selected:
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 3)
|
||||
archive_cell = self.archiveTable.item(row_selected[0].row(), 4)
|
||||
if archive_cell:
|
||||
archive_name = archive_cell.text()
|
||||
params = BorgListArchiveThread.prepare(profile)
|
||||
|
@ -298,8 +336,32 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
else:
|
||||
self._set_status(translate(params['message']))
|
||||
|
||||
dialog = choose_file_dialog(self, self.tr("Choose Extraction Point"))
|
||||
dialog = choose_file_dialog(self, self.tr("Choose Extraction Point"), want_folder=True)
|
||||
dialog.open(receive)
|
||||
|
||||
def extract_archive_result(self, result):
|
||||
self._toggle_all_buttons(True)
|
||||
|
||||
def update_mount_button_text(self):
|
||||
archive_name = self.selected_archive_name()
|
||||
if not archive_name:
|
||||
return
|
||||
|
||||
mode = 'Unmount' if archive_name in self.mount_points else 'Mount'
|
||||
self.set_mount_button_mode(mode)
|
||||
|
||||
def cell_double_clicked(self, row, column):
|
||||
if column == 3:
|
||||
archive_name = self.selected_archive_name()
|
||||
if not archive_name:
|
||||
return
|
||||
|
||||
mount_point = self.mount_points.get(archive_name)
|
||||
|
||||
if mount_point is not None:
|
||||
QDesktopServices.openUrl(QtCore.QUrl(f'file:///{mount_point}'))
|
||||
|
||||
def row_of_archive(self, archive_name):
|
||||
items = self.archiveTable.findItems(archive_name, QtCore.Qt.MatchExactly)
|
||||
rows = [item.row() for item in items if item.column() == 4]
|
||||
return rows[0] if rows else None
|
||||
|
|
Loading…
Reference in New Issue