mirror of https://github.com/borgbase/vorta
Add a button to mount full repository. By @real-yfprojects (#1257)
This commit is contained in:
parent
1b2d39e8f7
commit
6eaed0a8e9
|
@ -104,6 +104,22 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="bMountRepo">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">bMountRepo</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
|
@ -196,7 +212,7 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="bMount">
|
||||
<widget class="QToolButton" name="bMountArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
|
@ -204,7 +220,7 @@
|
|||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">bMount</string>
|
||||
<string notr="true">bMountArchive</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
|
|
|
@ -345,6 +345,7 @@ def format_archive_name(profile, archive_name_tpl):
|
|||
|
||||
def get_mount_points(repo_url):
|
||||
mount_points = {}
|
||||
repo_mounts = []
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
name = proc.name()
|
||||
|
@ -353,22 +354,30 @@ def get_mount_points(repo_url):
|
|||
continue
|
||||
|
||||
for idx, parameter in enumerate(proc.cmdline()):
|
||||
if parameter.startswith(repo_url + '::'):
|
||||
archive_name = parameter[len(repo_url) + 2:]
|
||||
if parameter.startswith(repo_url):
|
||||
# mount from this repo
|
||||
|
||||
# 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
|
||||
|
||||
# archive or full mount?
|
||||
if parameter[len(repo_url):].startswith('::'):
|
||||
archive_name = parameter[len(repo_url) + 2:]
|
||||
mount_points[archive_name] = mount_point
|
||||
break
|
||||
else:
|
||||
# repo mount point
|
||||
repo_mounts.append(mount_point)
|
||||
|
||||
except (psutil.ZombieProcess, psutil.AccessDenied, psutil.NoSuchProcess):
|
||||
# Getting process details may fail (e.g. zombie process on macOS)
|
||||
# or because the process is owned by another user.
|
||||
# Also see https://github.com/giampaolo/psutil/issues/783
|
||||
continue
|
||||
|
||||
return mount_points
|
||||
return mount_points, repo_mounts
|
||||
|
||||
|
||||
def is_system_tray_available():
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import os.path
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from typing import Dict
|
||||
from typing import Dict, Optional
|
||||
|
||||
from PyQt5 import QtCore, uic
|
||||
from PyQt5.QtCore import QMimeData, QPoint, Qt, pyqtSlot
|
||||
|
@ -46,7 +46,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
"""Init."""
|
||||
super().__init__(parent)
|
||||
self.setupUi(parent)
|
||||
self.mount_points = {}
|
||||
self.mount_points = {} # mount points of archives
|
||||
self.repo_mount_point: Optional[str] = None # mount point of whole repo
|
||||
self.menu = None
|
||||
self.app = app
|
||||
self.toolBox.setCurrentIndex(0)
|
||||
|
@ -86,7 +87,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
shortcut_copy.activated.connect(self.archive_copy)
|
||||
|
||||
# connect archive actions
|
||||
self.bMount.clicked.connect(self.bmount_clicked)
|
||||
self.bMountArchive.clicked.connect(self.bmountarchive_clicked)
|
||||
self.bRefreshArchive.clicked.connect(self.refresh_archive_info)
|
||||
self.bRename.clicked.connect(self.rename_action)
|
||||
self.bDelete.clicked.connect(self.delete_action)
|
||||
|
@ -98,6 +99,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
self.bPrune.clicked.connect(self.prune_action)
|
||||
self.bCheck.clicked.connect(self.check_action)
|
||||
self.bDiff.clicked.connect(self.diff_action)
|
||||
self.bMountRepo.clicked.connect(self.bmountrepo_clicked)
|
||||
|
||||
self.archiveTable.itemSelectionChanged.connect(self.on_selection_change)
|
||||
|
||||
|
@ -124,7 +126,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
self.bDelete.setIcon(get_colored_icon('trash'))
|
||||
self.bExtract.setIcon(get_colored_icon('cloud-download'))
|
||||
|
||||
self.bmount_refresh()
|
||||
self.bmountarchive_refresh()
|
||||
self.bmountrepo_refresh()
|
||||
|
||||
@pyqtSlot(QPoint)
|
||||
def archiveitem_contextmenu(self, pos: QPoint):
|
||||
|
@ -143,8 +146,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
menu.addAction(self.bRefreshArchive.icon(),
|
||||
self.bRefreshArchive.text(),
|
||||
self.refresh_archive_info)
|
||||
menu.addAction(self.bMount.icon(), self.bMount.text(),
|
||||
self.bmount_clicked)
|
||||
menu.addAction(self.bMountArchive.icon(), self.bMountArchive.text(),
|
||||
self.bmountarchive_clicked)
|
||||
menu.addAction(self.bExtract.icon(), self.bExtract.text(),
|
||||
self.extract_action)
|
||||
menu.addAction(self.bRename.icon(), self.bRename.text(),
|
||||
|
@ -174,7 +177,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
self.repoactions_enabled = enabled
|
||||
|
||||
for button in [self.bCheck, self.bList, self.bPrune,
|
||||
self.bDiff, self.fArchiveActions]:
|
||||
self.bDiff, self.fArchiveActions, self.bMountRepo]:
|
||||
button.setEnabled(enabled)
|
||||
button.repaint()
|
||||
|
||||
|
@ -185,7 +188,11 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
"""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)
|
||||
# get mount points
|
||||
self.mount_points, repo_mount_points = get_mount_points(profile.repo.url)
|
||||
if repo_mount_points:
|
||||
self.repo_mount_point = repo_mount_points[0]
|
||||
|
||||
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())]
|
||||
|
||||
|
@ -257,6 +264,9 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
for index in range(layout.count()):
|
||||
widget = layout.itemAt(index).widget()
|
||||
widget.setToolTip(self.tooltip_dict.get(widget, ""))
|
||||
|
||||
# refresh bMount for the selected archive
|
||||
self.bmountarchive_refresh()
|
||||
else:
|
||||
# too few or too many selected.
|
||||
self.fArchiveActions.setEnabled(False)
|
||||
|
@ -403,9 +413,9 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
return archive_cell.text()
|
||||
return None
|
||||
|
||||
def bmount_clicked(self):
|
||||
def bmountarchive_clicked(self):
|
||||
"""
|
||||
Handle `bMount` being clicked.
|
||||
Handle `bMountArchive` being clicked.
|
||||
|
||||
Mount or umount the current archive depending on its current state.
|
||||
"""
|
||||
|
@ -416,13 +426,24 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
return
|
||||
|
||||
if archive_name in self.mount_points:
|
||||
self.umount_action()
|
||||
self.unmount_action(archive_name=archive_name)
|
||||
else:
|
||||
self.mount_action(archive_name=archive_name)
|
||||
|
||||
def bmountrepo_clicked(self):
|
||||
"""
|
||||
Handle `bMountRepo` being clicked.
|
||||
|
||||
Mount or umount the repository depending on its current state.
|
||||
"""
|
||||
if self.repo_mount_point:
|
||||
self.unmount_action()
|
||||
else:
|
||||
self.mount_action()
|
||||
|
||||
def bmount_refresh(self):
|
||||
def bmountarchive_refresh(self):
|
||||
"""
|
||||
Update tooltip and state of `bMount`.
|
||||
Update label, tooltip and state of `bMount`.
|
||||
|
||||
The new state depends on the mount status of the current archive.
|
||||
This also updates the icon of the button.
|
||||
|
@ -430,35 +451,65 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
archive_name = self.selected_archive_name()
|
||||
|
||||
if archive_name in self.mount_points:
|
||||
self.bMount.setText(self.tr("Unmount"))
|
||||
self.bMount.setIcon(get_colored_icon('eject'))
|
||||
self.bMountArchive.setText(self.tr("Unmount"))
|
||||
self.bMountArchive.setIcon(get_colored_icon('eject'))
|
||||
self.bMountArchive.setToolTip(
|
||||
self.tr('Unmount the selected archive from the file system.'))
|
||||
else:
|
||||
self.bMount.setText(self.tr("Mount…"))
|
||||
self.bMount.setIcon(get_colored_icon('folder-open'))
|
||||
self.bMountArchive.setText(self.tr("Mount…"))
|
||||
self.bMountArchive.setIcon(get_colored_icon('folder-open'))
|
||||
self.bMountRepo.setToolTip(
|
||||
self.tr("Mount the selected archive " +
|
||||
"as a folder in the file system."))
|
||||
|
||||
def mount_action(self):
|
||||
def bmountrepo_refresh(self):
|
||||
"""
|
||||
Update label, tooltip and state of `bMount`.
|
||||
|
||||
The new state depends on the mount status of the current archive.
|
||||
This also updates the icon of the button.
|
||||
"""
|
||||
if self.repo_mount_point:
|
||||
self.bMountRepo.setText(self.tr("Unmount"))
|
||||
self.bMountRepo.setToolTip(
|
||||
self.tr('Unmount the repository from the file system.'))
|
||||
self.bMountRepo.setIcon(get_colored_icon('eject'))
|
||||
else:
|
||||
self.bMountRepo.setText(self.tr("Mount…"))
|
||||
self.bMountRepo.setIcon(get_colored_icon('folder-open'))
|
||||
self.bMountRepo.setToolTip(
|
||||
self.tr("Mount the repository as a folder in the file system."))
|
||||
|
||||
def mount_action(self, archive_name=None):
|
||||
"""
|
||||
Mount an archive or the whole repository.
|
||||
|
||||
Opens a file chooser to let the user choose a mount point and starts
|
||||
the borg job for mounting afterwards.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
archive_name : str, optional
|
||||
The archive to mount or None, by default None
|
||||
"""
|
||||
profile = self.profile()
|
||||
params = BorgMountJob.prepare(profile)
|
||||
if not params['ok']:
|
||||
self._set_status(params['message'])
|
||||
return
|
||||
|
||||
archive_name = self.selected_archive_name()
|
||||
|
||||
if not archive_name:
|
||||
# Conditions aren't met (borg binary available, etc)
|
||||
logger.debug('Archive name empty.')
|
||||
return
|
||||
|
||||
params['cmd'][-1] += f'::{archive_name}'
|
||||
params['current_archive'] = archive_name
|
||||
if archive_name:
|
||||
# mount archive
|
||||
params['cmd'][-1] += f'::{archive_name}'
|
||||
params['current_archive'] = archive_name
|
||||
# else mount complete repo
|
||||
|
||||
def receive():
|
||||
mount_point = dialog.selectedFiles()
|
||||
if mount_point:
|
||||
params['cmd'].append(mount_point[0])
|
||||
if params.get('current_archive', False):
|
||||
self.mount_points[params['current_archive']] = mount_point[0]
|
||||
params['mount_point'] = mount_point[0]
|
||||
|
||||
if params['ok']:
|
||||
self._toggle_all_buttons(False)
|
||||
job = BorgMountJob(params['cmd'], params, self.profile().repo.id)
|
||||
|
@ -472,20 +523,46 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
def mount_result(self, result):
|
||||
if result['returncode'] == 0:
|
||||
self._set_status(self.tr('Mounted successfully.'))
|
||||
|
||||
mount_point = result['params']['mount_point']
|
||||
|
||||
if result['params'].get('current_archive'):
|
||||
# archive was mounted
|
||||
archive_name = result['params']['current_archive']
|
||||
self.mount_points[archive_name] = mount_point
|
||||
|
||||
# update column in table
|
||||
archive_name = result['params']['current_archive']
|
||||
row = self.row_of_archive(archive_name)
|
||||
item = QTableWidgetItem(result['cmd'][-1])
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
# update button
|
||||
self.bmount_refresh()
|
||||
# update button
|
||||
self.bmountarchive_refresh()
|
||||
else:
|
||||
# whole repo was mounted
|
||||
self.repo_mount_point = mount_point
|
||||
self.bmountrepo_refresh()
|
||||
|
||||
self._toggle_all_buttons(True)
|
||||
|
||||
def umount_action(self):
|
||||
archive_name = self.selected_archive_name()
|
||||
mount_point = self.mount_points.get(archive_name)
|
||||
def unmount_action(self, archive_name=None):
|
||||
"""
|
||||
Unmount a (mounted) repository or archive.
|
||||
|
||||
If the target isn't mounted nothing happens.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
archive_name : str, optional
|
||||
The archive to unmount, by default None
|
||||
"""
|
||||
if archive_name:
|
||||
# unmount a single archive
|
||||
mount_point = self.mount_points.get(archive_name)
|
||||
else:
|
||||
# unmount the whole repository
|
||||
mount_point = self.repo_mount_point
|
||||
|
||||
if mount_point is not None:
|
||||
profile = self.profile()
|
||||
|
@ -494,10 +571,13 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
self._set_status(params['message'])
|
||||
return
|
||||
|
||||
params['current_archive'] = archive_name
|
||||
if archive_name:
|
||||
params['current_archive'] = archive_name
|
||||
params['mount_point'] = mount_point
|
||||
|
||||
if os.path.normpath(mount_point) in params['active_mount_points']:
|
||||
params['cmd'].append(mount_point)
|
||||
|
||||
job = BorgUmountJob(params['cmd'], params, self.profile().repo.id)
|
||||
job.updated.connect(self.mountErrors.setText)
|
||||
job.result.connect(self.umount_result)
|
||||
|
@ -508,20 +588,30 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
|
||||
def umount_result(self, result):
|
||||
self._toggle_all_buttons(True)
|
||||
archive_name = result['params']['current_archive']
|
||||
archive_name = result['params'].get('current_archive')
|
||||
mount_point = result['params']['mount_point']
|
||||
|
||||
if result['returncode'] == 0:
|
||||
self._set_status(self.tr('Un-mounted successfully.'))
|
||||
del self.mount_points[archive_name]
|
||||
row = self.row_of_archive(archive_name)
|
||||
item = QTableWidgetItem('')
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
# update button
|
||||
self.bmount_refresh()
|
||||
if archive_name:
|
||||
# unmount single archive
|
||||
del self.mount_points[archive_name]
|
||||
row = self.row_of_archive(archive_name)
|
||||
item = QTableWidgetItem('')
|
||||
self.archiveTable.setItem(row, 3, item)
|
||||
|
||||
# update button
|
||||
self.bmountarchive_refresh()
|
||||
else:
|
||||
# unmount repo
|
||||
self.repo_mount_point = None
|
||||
|
||||
self.bmountrepo_refresh()
|
||||
else:
|
||||
self._set_status(
|
||||
self.tr('Unmounting failed. Make sure no programs are using {}')
|
||||
.format(self.mount_points.get(archive_name)))
|
||||
.format(mount_point))
|
||||
|
||||
def save_prune_setting(self, new_value=None):
|
||||
profile = self.profile()
|
||||
|
|
|
@ -99,7 +99,7 @@ def test_check(qapp, mocker, borg_json_output, qtbot):
|
|||
qtbot.waitUntil(lambda: main.logText.text().startswith(success_text), **pytest._wait_defaults)
|
||||
|
||||
|
||||
def test_archive_mount(qapp, qtbot, mocker, borg_json_output, monkeypatch, choose_file_dialog):
|
||||
def test_mount(qapp, qtbot, mocker, borg_json_output, monkeypatch, choose_file_dialog):
|
||||
def psutil_disk_partitions(**kwargs):
|
||||
DiskPartitions = namedtuple('DiskPartitions', ['device', 'mountpoint'])
|
||||
return [DiskPartitions('borgfs', '/tmp')]
|
||||
|
@ -122,10 +122,16 @@ def test_archive_mount(qapp, qtbot, mocker, borg_json_output, monkeypatch, choos
|
|||
vorta.views.archive_tab, "choose_file_dialog", choose_file_dialog
|
||||
)
|
||||
|
||||
tab.mount_action()
|
||||
tab.bmountarchive_clicked()
|
||||
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), **pytest._wait_defaults)
|
||||
|
||||
tab.umount_action()
|
||||
tab.bmountarchive_clicked()
|
||||
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Un-mounted successfully.'), **pytest._wait_defaults)
|
||||
|
||||
tab.bmountrepo_clicked()
|
||||
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), **pytest._wait_defaults)
|
||||
|
||||
tab.bmountrepo_clicked()
|
||||
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Un-mounted successfully.'), **pytest._wait_defaults)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue