Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: quick mount #1664

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/vorta/assets/UI/archivetab.ui
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
</layout>
Expand Down Expand Up @@ -234,6 +237,9 @@
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
Expand Down
87 changes: 81 additions & 6 deletions src/vorta/views/archive_tab.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import logging
import os
import random
import shutil
import string
import sys
from datetime import timedelta
from typing import Dict, Optional
from PyQt5 import QtCore, uic
from PyQt5.QtCore import QItemSelectionModel, QMimeData, QPoint, Qt, pyqtSlot
from PyQt5.QtCore import QItemSelectionModel, QMimeData, QPoint, Qt, QUrl, pyqtSlot
from PyQt5.QtGui import QDesktopServices, QKeySequence
from PyQt5.QtWidgets import (
QAction,
Expand Down Expand Up @@ -106,7 +110,6 @@ def __init__(self, parent=None, app=None):
self.archiveTable.selectionModel().selectionChanged.connect(self.on_selection_change)

# connect archive actions
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)
Expand All @@ -118,7 +121,17 @@ def __init__(self, parent=None, app=None):
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.menuMountArchive = QMenu(self.bMountArchive)
self.menuMountArchive.addAction(translate("MountArchive", "Mount to Folder…"), self.bmountarchive_clicked)
self.menuMountArchive.addAction(
translate("MountArchive", "Quick Mount…"), lambda: self.bmountarchive_clicked(True)
)
self.bMountArchive.setMenu(self.menuMountArchive)

self.menuMountRepo = QMenu(self.bMountRepo)
self.menuMountRepo.addAction(translate("MountRepo", "Mount to Folder…"), self.bmountrepo_clicked)
self.menuMountRepo.addAction(translate("MountRepo", "Quick Mount…"), self.quick_mount_action)
self.bMountRepo.setMenu(self.menuMountRepo)

self.archiveNameTemplate.textChanged.connect(
lambda tpl, key='new_archive_name': self.save_archive_template(tpl, key)
Expand Down Expand Up @@ -366,6 +379,7 @@ def on_selection_change(self, selected=None, deselected=None):

# special treatment for dynamic mount/unmount button.
self.bmountarchive_refresh()
self.bmountrepo_refresh()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that needed? I guess it doesn't hurt either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To support changing the repository from the dropdown in theRepository tab. Without this, the Unmount option is visible for all repositories when I restart Vorta and not just the one I mounted.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would make more sense in _toggle_all_buttons

tooltip = self.bMountArchive.toolTip()
self.bMountArchive.setToolTip(tooltip + " " + self.tr("(Select exactly one archive)"))

Expand Down Expand Up @@ -502,7 +516,7 @@ def selected_archive_name(self):
return archive_cell.text()
return None

def bmountarchive_clicked(self):
def bmountarchive_clicked(self, quick=False):
"""
Handle `bMountArchive` being clicked.

Expand All @@ -516,9 +530,46 @@ def bmountarchive_clicked(self):

if archive_name in self.mount_points:
self.unmount_action(archive_name=archive_name)
elif quick:
self.quick_mount_action(archive_name=archive_name)
else:
self.mount_action(archive_name=archive_name)

def get_vorta_quick_mountpoint(self):
"""
return a temporary directory in the user's home folder ~/vorta-quick-mount-{randomcharacters}
"""
return os.path.join(
os.path.expanduser('~'),
'vorta-quick-mount-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)),
)

def quick_mount_action(self, archive_name=None):
"""
mount the selected archive or repository to a temporary directory.
"""
profile = self.profile()
params = BorgMountJob.prepare(profile, archive=archive_name)
if not params['ok']:
self._set_status(params['message'])
return

mount_point = self.get_vorta_quick_mountpoint()
while os.path.exists(mount_point) and os.listdir(mount_point):
mount_point = self.get_vorta_quick_mountpoint()

os.mkdir(mount_point)

params['cmd'].append(mount_point)
params['mount_point'] = mount_point

if params['ok']:
self._toggle_all_buttons(False)
job = BorgMountJob(params['cmd'], params, self.profile().repo.id)
job.updated.connect(self.mountErrors.setText)
job.result.connect(lambda result: self.mount_result(result, quick=True))
self.app.jobs_manager.add_job(job)

def bmountrepo_clicked(self):
"""
Handle `bMountRepo` being clicked.
Expand All @@ -544,11 +595,22 @@ def bmountarchive_refresh(self, icon_only=False):
if not icon_only:
self.bMountArchive.setText(self.tr("Unmount"))
self.bMountArchive.setToolTip(self.tr('Unmount the selected archive from the file system'))
self.bMountArchive.setMenu(None)
try:
self.bMountArchive.clicked.disconnect(self.bmountarchive_clicked) # avoid race condition
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would bmountarchive_clicked be connected to the signal multiple times?
This also doesn't disconnect the slot lambda: self.bmountarchive_clicked(quick=True) afaik.

self.bMountArchive.clicked.connect(self.bmountarchive_clicked)
except TypeError:
self.bMountArchive.clicked.connect(self.bmountarchive_clicked)
Comment on lines +645 to +647
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there two calls to connect?

else:
self.bMountArchive.setIcon(get_colored_icon('folder-open'))
if not icon_only:
self.bMountArchive.setText(self.tr("Mount…"))
self.bMountArchive.setToolTip(self.tr("Mount the selected archive " + "as a folder in the file system"))
try:
self.bMountArchive.clicked.disconnect(self.bmountarchive_clicked)
self.bMountArchive.setMenu(self.menuMountArchive)
except TypeError: # when bMountArchive.clicked is not connected
pass

def bmountrepo_refresh(self):
"""
Expand All @@ -561,10 +623,18 @@ def bmountrepo_refresh(self):
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'))
self.bMountRepo.setMenu(None)
self.bMountRepo.clicked.connect(self.bmountrepo_clicked)
else:
try:
# disconnect the button to open the menu
self.bMountRepo.clicked.disconnect(self.bmountrepo_clicked)
except TypeError: # on first run, when the button is not connected
pass
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"))
self.bMountRepo.setMenu(self.menuMountRepo)

def mount_action(self, archive_name=None):
"""
Expand Down Expand Up @@ -600,12 +670,16 @@ def receive():
dialog = choose_file_dialog(self, self.tr("Choose Mount Point"), want_folder=True)
dialog.open(receive)

def mount_result(self, result):
def mount_result(self, result, quick=False):
if result['returncode'] == 0:
self._set_status(self.tr('Mounted successfully.'))

mount_point = result['params']['mount_point']

if quick:
# open the folder
QDesktopServices.openUrl(QUrl.fromLocalFile(mount_point))

if result['params'].get('mounted_archive'):
# archive was mounted
archive_name = result['params']['mounted_archive']
Expand Down Expand Up @@ -662,6 +736,8 @@ def umount_result(self, result):

if result['returncode'] == 0:
self._set_status(self.tr('Un-mounted successfully.'))
if os.path.basename(mount_point).startswith("vorta-quick-mount-"):
shutil.rmtree(mount_point)

if archive_name:
# unmount single archive
Expand All @@ -670,7 +746,6 @@ def umount_result(self, result):
item = QTableWidgetItem('')
self.archiveTable.setItem(row, 3, item)

# update button
self.bmountarchive_refresh()
else:
# unmount repo
Expand Down