Implement adding ssh keys. Start adding mounting.

This commit is contained in:
Manu 2018-10-27 15:00:56 +08:00
parent 443b61a60c
commit 8cede50bd1
9 changed files with 307 additions and 87 deletions

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ build/
dist/
docs/
*.autosave
vorta/libs
bin/
__pycache__

View File

@ -5,11 +5,13 @@ block_cipher = None
a = Analysis(['vorta/__main__.py'],
pathex=['/Users/manu/Workspace/vorta'],
binaries=[],
datas=[
('vorta/UI/*.ui', 'vorta/UI')
binaries=[
('bin/macosx64/borg', 'bin')
],
hiddenimports=['borg.archiver'],
datas=[
('vorta/UI/*.ui', 'vorta/UI'),
],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
@ -40,3 +42,12 @@ app = BUNDLE(exe,
'NSHighResolutionCapable': 'True'
},
)
# Debug package. (inspired from borg)
if False:
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='vorta-dir')

View File

@ -39,7 +39,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>1</number>
<number>2</number>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
@ -385,18 +385,58 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Refresh</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;To mount snapshots, first install &amp;quot;FUSE for macOS&amp;quot; from &lt;a href=&quot;https://osxfuse.github.io/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;here&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="pushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Mount</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignLeft">
<widget class="QPushButton" name="pushButton_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<width>450</width>
<height>300</height>
</rect>
</property>
@ -18,8 +18,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>401</width>
<height>261</height>
<width>451</width>
<height>281</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -35,8 +35,30 @@
<property name="bottomMargin">
<number>10</number>
</property>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Generate SSH Key</string>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
@ -45,7 +67,14 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox"/>
<widget class="QComboBox" name="formatSelect">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
@ -55,56 +84,90 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_2"/>
<widget class="QComboBox" name="lengthSelect">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;2048 or 4096 for RSA, 384 or 521 for ECDSA. Fixed for Ed25519. &lt;a href=&quot;https://stribika.github.io/2015/01/04/secure-secure-shell.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;More&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="outputFileTextBox"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output File:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Don't change this if you want SSH to automatically find the key.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<item alignment="Qt::AlignBottom">
<widget class="QLabel" name="errors">
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generateButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Generate and copy to Clipboard</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>

View File

@ -1,5 +1,7 @@
import json
import os
import sys
import shutil
from PyQt5 import QtCore
import subprocess
from subprocess import Popen, PIPE
@ -11,6 +13,12 @@ class BorgThread(QtCore.QThread):
def __init__(self, parent, cmd, params):
super().__init__(parent)
# Find packaged borg binary. Prefer globally installed.
if not shutil.which('borg'):
meipass_borg = os.path.join(sys._MEIPASS, 'bin', 'borg')
if os.path.isfile(meipass_borg):
cmd[0] = meipass_borg
self.cmd = cmd
env = os.environ.copy()
@ -23,7 +31,6 @@ class BorgThread(QtCore.QThread):
self.params = params
def run(self):
self.updated.emit('Adding Repo...')
with Popen(self.cmd, stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True, env=self.env) as p:
for line in p.stderr:
print(line)

View File

@ -2,13 +2,14 @@ import sys
import os
import platform
from datetime import datetime as dt
from PyQt5.QtWidgets import QApplication, QFileDialog, QTableWidgetItem
from PyQt5 import uic, QtCore
from PyQt5.QtWidgets import QApplication, QFileDialog, QTableWidgetItem, QTableView
from PyQt5 import uic, QtCore, QtGui
from .repo_add import AddRepoWindow, ExistingRepoWindow
from .repo_init import InitRepoWindow
from .ssh_add import SSHAddWindow
from .config import APP_NAME, reset_app
from .models import RepoModel, SourceDirModel, SnapshotModel, BackupConfigModel
from .models import RepoModel, SourceDirModel, SnapshotModel, BackupProfileModel
from .ssh_keys import get_private_keys
from .borg_runner import BorgThread
@ -22,7 +23,7 @@ class MainWindow(MainWindowBase, MainWindowUI):
super().__init__()
self.setupUi(self)
self.setWindowTitle(APP_NAME)
self.config = BackupConfigModel.get(id=1)
self.profile = BackupProfileModel.get(id=1)
self.init_repo()
self.init_source()
self.init_snapshots()
@ -67,9 +68,9 @@ class MainWindow(MainWindowBase, MainWindowUI):
for repo in RepoModel.select():
self.repoSelector.addItem(repo.url, repo.id)
if self.config.repo:
self.repoSelector.setCurrentIndex(self.repoSelector.findData(self.config.repo.id))
self.repoSelector.currentIndexChanged.connect(self.repo_add_action)
if self.profile.repo:
self.repoSelector.setCurrentIndex(self.repoSelector.findData(self.profile.repo.id))
self.repoSelector.currentIndexChanged.connect(self.repo_select_action)
def init_source(self):
self.sourceAdd.clicked.connect(self.source_add)
@ -81,6 +82,20 @@ class MainWindow(MainWindowBase, MainWindowUI):
keys = get_private_keys()
for key in keys:
self.sshComboBox.addItem(f'{key["filename"]} ({key["format"]}:{key["fingerprint"]})', key['filename'])
self.sshComboBox.currentIndexChanged.connect(self.ssh_select_action)
def ssh_select_action(self, index):
if index == 1:
ssh_add_window = SSHAddWindow()
ssh_add_window.setParent(self, QtCore.Qt.Sheet)
ssh_add_window.show()
if ssh_add_window.exec_():
self.init_ssh()
else:
self.profile.ssh_key = self.sshComboBox.itemData(index)
self.profile.save()
print('set ssh key to', self.profile.ssh_key)
def init_snapshots(self):
snapshots = [s for s in SnapshotModel.select()]
@ -92,37 +107,39 @@ class MainWindow(MainWindowBase, MainWindowUI):
self.snapshotTable.setItem(row, 1, QTableWidgetItem(snapshot.time))
self.snapshotTable.resizeColumnsToContents()
self.snapshotTable.horizontalHeader().setVisible(True)
self.snapshotTable.setSelectionBehavior(QTableView.SelectRows)
self.snapshotTable.setEditTriggers(QTableView.NoEditTriggers)
def init_compression(self):
self.repoCompression.addItem('LZ4 (default)', 'lz4')
self.repoCompression.addItem('Zstandard (medium)', 'zstd')
self.repoCompression.addItem('LZMA (high)', 'lzma,6')
def repo_add_action(self, index):
def repo_select_action(self, index):
if index <= 2:
if index == 1:
self.addRepoWindow = AddRepoWindow()
repo_add_window = AddRepoWindow()
else:
self.addRepoWindow = ExistingRepoWindow()
repo_add_window = ExistingRepoWindow()
self.addRepoWindow.setParent(self, QtCore.Qt.Sheet)
self.addRepoWindow.show()
if self.addRepoWindow.exec_():
params = self.addRepoWindow.get_values()
repo_add_window.setParent(self, QtCore.Qt.Sheet)
repo_add_window.show()
if repo_add_window.exec_():
params = repo_add_window.get_values()
if index == 1:
cmd = ["borg", "init", "--log-json", f"--encryption={params['encryption']}", params['repo_url']]
else:
cmd = ["borg", "list", "--json", params['repo_url']]
initwindow = InitRepoWindow(cmd, params)
initwindow.setParent(self, QtCore.Qt.Sheet)
initwindow.show()
initwindow._thread.start()
initwindow._thread.result.connect(self.repo_add_result)
init_window = InitRepoWindow(cmd, params)
init_window.setParent(self, QtCore.Qt.Sheet)
init_window.show()
init_window._thread.start()
init_window._thread.result.connect(self.repo_add_result)
else:
self.config.repo = self.repoSelector.currentData()
self.config.save()
self.profile.repo = self.repoSelector.currentData()
self.profile.save()
def repo_add_result(self, result):
print(result)
@ -169,4 +186,4 @@ class MainWindow(MainWindowBase, MainWindowUI):
def menu_reset(self):
reset_app()
app.quit()
QApplication.instance().quit()

View File

@ -17,11 +17,12 @@ class RepoModel(peewee.Model):
database = db
class BackupConfigModel(peewee.Model):
class BackupProfileModel(peewee.Model):
"""Allows the user to switch between different configurations."""
name = peewee.CharField()
added_at = peewee.DateTimeField(default=datetime.utcnow)
repo = peewee.ForeignKeyField(RepoModel, default=None, null=True)
ssh_key = peewee.CharField(default=None, null=True)
class Meta:
database = db
@ -30,7 +31,7 @@ class BackupConfigModel(peewee.Model):
class SourceDirModel(peewee.Model):
"""A folder to be backed up, related to a Backup Configuration."""
dir = peewee.CharField()
config = peewee.ForeignKeyField(BackupConfigModel, default=1)
config = peewee.ForeignKeyField(BackupProfileModel, default=1)
added_at = peewee.DateTimeField(default=datetime.utcnow)
class Meta:
@ -49,6 +50,6 @@ class SnapshotModel(peewee.Model):
db.connect()
db.create_tables([RepoModel, BackupConfigModel, SourceDirModel, SnapshotModel])
db.create_tables([RepoModel, BackupProfileModel, SourceDirModel, SnapshotModel])
BackupConfigModel.get_or_create(id=1, name='Default')
BackupProfileModel.get_or_create(id=1, name='Default')

80
vorta/ssh_add.py Normal file
View File

@ -0,0 +1,80 @@
import os
import subprocess
from PyQt5 import uic, QtCore
from PyQt5.QtCore import QProcess
from PyQt5.QtWidgets import QApplication
from paramiko.rsakey import RSAKey
from paramiko.ecdsakey import ECDSAKey
from paramiko.ed25519key import Ed25519Key
uifile = os.path.join(os.path.dirname(__file__), 'UI/sshadd.ui')
SSHAddUI, SSHAddBase = uic.loadUiType(uifile)
FORMAT_MAPPING = {
'ed25519': Ed25519Key,
'rsa': RSAKey,
'ecdsa': ECDSAKey
}
class SSHAddWindow(SSHAddBase, SSHAddUI):
def __init__(self):
super().__init__()
self.setupUi(self)
self.closeButton.clicked.connect(self.accept)
self.generateButton.clicked.connect(self.generate_key)
self.init_format()
self.init_length()
def init_format(self):
self.formatSelect.addItem('ED25519 (Recommended)', 'ed25519')
self.formatSelect.addItem('RSA (Legacy)', 'rsa')
self.formatSelect.addItem('ECDSA', 'ecdsa')
self.outputFileTextBox.setText('~/.ssh/id_ed25519')
self.formatSelect.currentIndexChanged.connect(self.format_select_change)
def format_select_change(self, index):
new_output = f'~/.ssh/id_{self.formatSelect.currentData()}'
self.outputFileTextBox.setText(new_output)
def init_length(self):
self.lengthSelect.addItem('High (Recommended)', ('4096', '521'))
self.lengthSelect.addItem('Medium', ('2048', '384'))
def generate_key(self):
format = self.formatSelect.currentData()
length = self.lengthSelect.currentData()
if format == 'rsa':
length = length[0]
else:
length = length[1]
output_path = os.path.expanduser(self.outputFileTextBox.text())
if os.path.isfile(output_path):
self.errors.setText('Key file already exists. Not overwriting.')
else:
self.sshproc = QProcess(self)
self.sshproc.finished.connect(self.generate_key_result)
self.sshproc.start('ssh-keygen', ['-t', format, '-b', length, '-f', output_path, '-N', ''])
def generate_key_result(self, exitCode, exitStatus):
if exitCode == 0:
output_path = os.path.expanduser(self.outputFileTextBox.text())
pub_key = open(output_path+'.pub').read().strip()
clipboard = QApplication.clipboard()
clipboard.setText(pub_key)
self.errors.setText(f'New key was copied to clipboard and written to {output_path}.')
else:
self.errors.setText('Error during key generation.')
def get_values(self):
return {
'ssh_key': self.sshComboBox.currentData(),
'encryption': self.encryptionComboBox.currentData(),
'repo_url': self.repoURL.text(),
'password': self.passwordLineEdit.text()
}

View File

@ -8,7 +8,7 @@ from paramiko import SSHException
def get_private_keys():
key_formats = [RSAKey, ECDSAKey, Ed25519Key]
ssh_folder = os.path.join(os.path.expanduser('~'), '.ssh')
ssh_folder = os.path.expanduser('~/.ssh')
available_private_keys = []
for key in os.listdir(ssh_folder):