Improve archive mount feature. Fixes #131. By @antlarr (#156)

This commit is contained in:
Manuel Riel 2019-01-21 18:44:01 +08:00 committed by GitHub
parent 6e0904647b
commit ab368b32a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 30 deletions

View File

@ -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>

View File

@ -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.')

View File

@ -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

View File

@ -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