mirror of https://github.com/borgbase/vorta
Working version with snapshots and ssh creation.
This commit is contained in:
parent
8cede50bd1
commit
1d0a6b5f45
|
@ -5,11 +5,10 @@
|
||||||
- Select and manage SSH keys
|
- Select and manage SSH keys
|
||||||
- Initialize new remote Borg repositories
|
- Initialize new remote Borg repositories
|
||||||
- Create new Borg snapshots (backups) from local folders
|
- Create new Borg snapshots (backups) from local folders
|
||||||
- Display existing snapshots and repository details.
|
- Mount existing snapshots with FUSE
|
||||||
- Settings stored in sqlite
|
- Settings stored in sqlite
|
||||||
- Borg binary integrated
|
|
||||||
|
|
||||||
Planned features:
|
Missing features:
|
||||||
|
|
||||||
- Scheduling for background backups.
|
- Scheduling for background backups.
|
||||||
- Rule-based scheduling by time, Wifi SSID, etc.
|
- Rule-based scheduling by time, Wifi SSID, etc.
|
||||||
|
@ -18,6 +17,7 @@ Planned features:
|
||||||
- Securely save repo password in Keychain instead of database.
|
- Securely save repo password in Keychain instead of database.
|
||||||
- Handle encrypted SSH keys
|
- Handle encrypted SSH keys
|
||||||
- Check for duplicate source dirs
|
- Check for duplicate source dirs
|
||||||
|
- Exclude options/patterns.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
Conda is used for dependency management. Create a new virtual env using:
|
Conda is used for dependency management. Create a new virtual env using:
|
||||||
|
@ -36,6 +36,9 @@ To build a binary package:
|
||||||
$ pyinstaller --clean --noconfirm vorta.spec
|
$ pyinstaller --clean --noconfirm vorta.spec
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Why the Name?
|
||||||
|
[Vorta](http://memory-alpha.wikia.com/wiki/Vorta) are a race referenced in Star Trek. They serve the Dominion and are replaced by their clones if they die. Just like our backups.
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
(C) 2018 Manuel Riel for [BorgBase.com](https://www.borgbase.com)
|
(C) 2018 Manuel Riel for [BorgBase.com](https://www.borgbase.com)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ dependencies:
|
||||||
- paramiko
|
- paramiko
|
||||||
- pyqt5
|
- pyqt5
|
||||||
- peewee
|
- peewee
|
||||||
|
- python-dateutil
|
||||||
|
|
||||||
dev-dependencies:
|
dev-dependencies:
|
||||||
- pyinstaller
|
- pyinstaller
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTabWidget" name="tabWidget">
|
<widget class="QTabWidget" name="tabWidget">
|
||||||
<property name="currentIndex">
|
<property name="currentIndex">
|
||||||
<number>2</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="tab_2">
|
<widget class="QWidget" name="tab_2">
|
||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
|
@ -81,104 +81,82 @@
|
||||||
<number>25</number>
|
<number>25</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_8">
|
<widget class="QLabel" name="label_10">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Repository ID:</string>
|
<string>Encryption:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLabel" name="repoId">
|
<widget class="QLabel" name="repoEncryption">
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_9">
|
|
||||||
<property name="text">
|
|
||||||
<string>Location:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="repoLocation">
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="label_10">
|
<widget class="Line" name="line">
|
||||||
<property name="text">
|
<property name="sizePolicy">
|
||||||
<string>Encrypted:</string>
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QLabel" name="repoEncryption_2">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="Line" name="line">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="Line" name="line_2">
|
<widget class="Line" name="line_2">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QLabel" name="label_11">
|
<widget class="QLabel" name="label_11">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Original Size:</string>
|
<string>Original Size:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="3" column="1">
|
||||||
<widget class="QLabel" name="label_12">
|
<widget class="QLabel" name="sizeOriginal">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Compressed Size:</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label_13">
|
<widget class="QLabel" name="label_13">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Deduplicated Size:</string>
|
<string>Deduplicated Size:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="sizeDeduplicated">
|
<widget class="QLabel" name="label_12">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string>Compressed Size:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QLabel" name="sizeCompressed">
|
<widget class="QLabel" name="sizeCompressed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QLabel" name="sizeOriginal">
|
<widget class="QLabel" name="sizeDeduplicated">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
|
@ -385,7 +363,10 @@
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="mountErrors">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string><html><head/><body><p>To mount snapshots, first install &quot;FUSE for macOS&quot; from <a href="https://osxfuse.github.io/"><span style=" text-decoration: underline; color:#0000ff;">here</span></a>.</p></body></html></string>
|
<string><html><head/><body><p>To mount snapshots, first install &quot;FUSE for macOS&quot; from <a href="https://osxfuse.github.io/"><span style=" text-decoration: underline; color:#0000ff;">here</span></a>.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -394,7 +375,7 @@
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="pushButton">
|
<widget class="QPushButton" name="snapshotMountButton">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
|
@ -407,7 +388,7 @@
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="pushButton_2">
|
<widget class="QPushButton" name="snapshotDeleteButton">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
|
@ -423,7 +404,10 @@
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item alignment="Qt::AlignLeft">
|
<item alignment="Qt::AlignLeft">
|
||||||
<widget class="QPushButton" name="pushButton_4">
|
<widget class="QPushButton" name="snapshotRefreshButton">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
|
@ -449,6 +433,43 @@
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="palette">
|
||||||
|
<palette>
|
||||||
|
<active>
|
||||||
|
<colorrole role="Highlight">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="255">
|
||||||
|
<red>255</red>
|
||||||
|
<green>151</green>
|
||||||
|
<blue>41</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</active>
|
||||||
|
<inactive>
|
||||||
|
<colorrole role="Highlight">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="255">
|
||||||
|
<red>255</red>
|
||||||
|
<green>151</green>
|
||||||
|
<blue>41</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</inactive>
|
||||||
|
<disabled>
|
||||||
|
<colorrole role="Highlight">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="255">
|
||||||
|
<red>220</red>
|
||||||
|
<green>220</green>
|
||||||
|
<blue>220</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</disabled>
|
||||||
|
</palette>
|
||||||
|
</property>
|
||||||
<property name="value">
|
<property name="value">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -497,7 +518,7 @@
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuBackup_Configuration">
|
<widget class="QMenu" name="menuBackup_Configuration">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Backup Configuration</string>
|
<string>Backup Profile</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="actionLatest"/>
|
<addaction name="actionLatest"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
|
@ -43,7 +43,9 @@ class BorgThread(QtCore.QThread):
|
||||||
'params': self.params,
|
'params': self.params,
|
||||||
'returncode': p.returncode,
|
'returncode': p.returncode,
|
||||||
}
|
}
|
||||||
if stdout.strip():
|
try:
|
||||||
result['data'] = json.loads(stdout)
|
result['data'] = json.loads(stdout)
|
||||||
|
except:
|
||||||
|
result['data'] = {}
|
||||||
|
|
||||||
self.result.emit(result)
|
self.result.emit(result)
|
||||||
|
|
177
vorta/main.py
177
vorta/main.py
|
@ -1,17 +1,17 @@
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
from PyQt5.QtWidgets import QApplication, QFileDialog, QTableWidgetItem, QTableView
|
from dateutil import parser
|
||||||
from PyQt5 import uic, QtCore, QtGui
|
from PyQt5.QtWidgets import QApplication, QFileDialog, QTableWidgetItem, QTableView, QTableWidget
|
||||||
|
from PyQt5 import uic, QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from .repo_add import AddRepoWindow, ExistingRepoWindow
|
from .repo_add import AddRepoWindow, ExistingRepoWindow
|
||||||
from .repo_init import InitRepoWindow
|
|
||||||
from .ssh_add import SSHAddWindow
|
from .ssh_add import SSHAddWindow
|
||||||
from .config import APP_NAME, reset_app
|
from .config import APP_NAME, reset_app
|
||||||
from .models import RepoModel, SourceDirModel, SnapshotModel, BackupProfileModel
|
from .models import RepoModel, SourceDirModel, SnapshotModel, BackupProfileModel
|
||||||
from .ssh_keys import get_private_keys
|
from .ssh_keys import get_private_keys
|
||||||
from .borg_runner import BorgThread
|
from .borg_runner import BorgThread
|
||||||
|
from .utils import prettyByes
|
||||||
|
|
||||||
|
|
||||||
uifile = os.path.join(os.path.dirname(__file__), 'UI/mainwindow.ui')
|
uifile = os.path.join(os.path.dirname(__file__), 'UI/mainwindow.ui')
|
||||||
|
@ -33,44 +33,59 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
||||||
self.createStartBtn.clicked.connect(self.create_action)
|
self.createStartBtn.clicked.connect(self.create_action)
|
||||||
self.actionResetApp.triggered.connect(self.menu_reset)
|
self.actionResetApp.triggered.connect(self.menu_reset)
|
||||||
|
|
||||||
|
def set_status(self, text=None, progress_max=None):
|
||||||
|
if text:
|
||||||
|
self.createProgressText.setText(text)
|
||||||
|
if progress_max is not None:
|
||||||
|
self.createProgress.setRange(0, progress_max)
|
||||||
|
self.createProgressText.repaint()
|
||||||
|
|
||||||
def create_action(self):
|
def create_action(self):
|
||||||
|
n_backup_folders = self.sourceDirectoriesWidget.count()
|
||||||
|
if n_backup_folders == 0:
|
||||||
|
self.set_status('Add some folders to back up first.')
|
||||||
|
return
|
||||||
|
self.set_status('Starting Backup.', progress_max=0)
|
||||||
|
self.createStartBtn.setEnabled(False)
|
||||||
|
self.createStartBtn.repaint()
|
||||||
|
|
||||||
repo_id = self.repoSelector.currentData()
|
repo_id = self.repoSelector.currentData()
|
||||||
repo = RepoModel.get(id=repo_id)
|
repo = RepoModel.get(id=repo_id)
|
||||||
cmd = ['borg', 'create', '--log-json', '--json',
|
cmd = ['borg', 'create', '--log-json', '--json', '-C', self.profile.compression,
|
||||||
f'{repo.url}::{platform.node()}-{dt.now().isoformat()}'
|
f'{repo.url}::{platform.node()}-{dt.now().isoformat()}'
|
||||||
]
|
]
|
||||||
for i in range(self.sourceDirectoriesWidget.count()):
|
for i in range(n_backup_folders):
|
||||||
cmd.append(self.sourceDirectoriesWidget.item(i).text())
|
cmd.append(self.sourceDirectoriesWidget.item(i).text())
|
||||||
self.createProgress.setRange(0, 0)
|
|
||||||
thread = BorgThread(self, cmd, {})
|
thread = BorgThread(self, cmd, {})
|
||||||
thread.updated.connect(self.create_update_log)
|
thread.updated.connect(self.create_update_log)
|
||||||
thread.result.connect(self.create_get_result)
|
thread.result.connect(self.create_get_result)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def create_update_log(self, text):
|
def create_update_log(self, text):
|
||||||
self.createProgressText.setText(text)
|
self.set_status(text)
|
||||||
|
|
||||||
def create_get_result(self, result):
|
def create_get_result(self, result):
|
||||||
self.createProgress.setRange(0, 100)
|
self.createStartBtn.setEnabled(True)
|
||||||
print(result)
|
self.createStartBtn.repaint()
|
||||||
if result['returncode'] == 0:
|
if result['returncode'] == 0:
|
||||||
self.createProgressText.setText('Finished backup.')
|
self.set_status('Finished backup.', 100)
|
||||||
new_snapshot = SnapshotModel(
|
new_snapshot = SnapshotModel(
|
||||||
|
snapshot_id=result['data']['archive']['id'],
|
||||||
name=result['data']['archive']['name'],
|
name=result['data']['archive']['name'],
|
||||||
time=result['data']['archive']['start'],
|
time=parser.parse(result['data']['archive']['start']),
|
||||||
repo=self.repoSelector.currentData()
|
repo=self.repoSelector.currentData()
|
||||||
)
|
)
|
||||||
|
new_snapshot.save()
|
||||||
def init_repo(self):
|
if 'cache' in result['data']:
|
||||||
self.repoSelector.model().item(0).setEnabled(False)
|
stats = result['data']['cache']['stats']
|
||||||
self.repoSelector.addItem('Initialize New Repository', 'init')
|
repo = self.profile.repo
|
||||||
self.repoSelector.addItem('Add Existing Repository', 'existing')
|
repo.total_size = stats['total_size']
|
||||||
for repo in RepoModel.select():
|
repo.unique_csize = stats['unique_csize']
|
||||||
self.repoSelector.addItem(repo.url, repo.id)
|
repo.unique_size = stats['unique_size']
|
||||||
|
repo.total_unique_chunks = stats['total_unique_chunks']
|
||||||
if self.profile.repo:
|
repo.save()
|
||||||
self.repoSelector.setCurrentIndex(self.repoSelector.findData(self.profile.repo.id))
|
self.init_snapshots()
|
||||||
self.repoSelector.currentIndexChanged.connect(self.repo_select_action)
|
|
||||||
|
|
||||||
def init_source(self):
|
def init_source(self):
|
||||||
self.sourceAdd.clicked.connect(self.source_add)
|
self.sourceAdd.clicked.connect(self.source_add)
|
||||||
|
@ -96,24 +111,90 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
print('set ssh key to', self.profile.ssh_key)
|
print('set ssh key to', self.profile.ssh_key)
|
||||||
|
|
||||||
|
|
||||||
def init_snapshots(self):
|
def init_snapshots(self):
|
||||||
snapshots = [s for s in SnapshotModel.select()]
|
if self.profile.repo:
|
||||||
self.snapshotTable.setRowCount(len(snapshots))
|
snapshots = [s for s in self.profile.repo.snapshots.select()]
|
||||||
|
|
||||||
|
for row, snapshot in enumerate(snapshots):
|
||||||
|
self.snapshotTable.insertRow(row)
|
||||||
|
self.snapshotTable.setItem(row, 0, QTableWidgetItem(snapshot.name))
|
||||||
|
formatted_time = snapshot.time.strftime('%Y-%m-%d %H:%M')
|
||||||
|
self.snapshotTable.setItem(row, 1, QTableWidgetItem(formatted_time))
|
||||||
|
|
||||||
|
self.sizeCompressed.setText(prettyByes(self.profile.repo.unique_csize))
|
||||||
|
self.sizeDeduplicated.setText(prettyByes(self.profile.repo.unique_size))
|
||||||
|
self.sizeOriginal.setText(prettyByes(self.profile.repo.total_size))
|
||||||
|
self.repoEncryption.setText(str(self.profile.repo.encryption))
|
||||||
|
self.snapshotTable.setRowCount(len(snapshots))
|
||||||
|
|
||||||
|
header = self.snapshotTable.horizontalHeader()
|
||||||
|
header.setVisible(True)
|
||||||
|
header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
for row, snapshot in enumerate(snapshots):
|
|
||||||
self.snapshotTable.insertRow(row)
|
|
||||||
self.snapshotTable.setItem(row, 0, QTableWidgetItem(snapshot.name))
|
|
||||||
self.snapshotTable.setItem(row, 1, QTableWidgetItem(snapshot.time))
|
|
||||||
self.snapshotTable.resizeColumnsToContents()
|
|
||||||
self.snapshotTable.horizontalHeader().setVisible(True)
|
|
||||||
self.snapshotTable.setSelectionBehavior(QTableView.SelectRows)
|
self.snapshotTable.setSelectionBehavior(QTableView.SelectRows)
|
||||||
self.snapshotTable.setEditTriggers(QTableView.NoEditTriggers)
|
self.snapshotTable.setEditTriggers(QTableView.NoEditTriggers)
|
||||||
|
|
||||||
|
self.snapshotMountButton.clicked.connect(self.snapshot_mount)
|
||||||
|
self.snapshotDeleteButton.clicked.connect(self.snapshot_mount)
|
||||||
|
self.snapshotRefreshButton.clicked.connect(self.snapshot_mount)
|
||||||
|
|
||||||
|
def snapshot_mount(self):
|
||||||
|
cmd = ['borg', 'mount', '--log-json']
|
||||||
|
row_selected = self.snapshotTable.selectionModel().selectedRows()
|
||||||
|
if row_selected:
|
||||||
|
snapshot_cell = self.snapshotTable.item(row_selected[0].row(), 0)
|
||||||
|
if snapshot_cell:
|
||||||
|
snapshot_name = snapshot_cell.text()
|
||||||
|
cmd.append(f'{self.profile.repo.url}::{snapshot_name}')
|
||||||
|
else:
|
||||||
|
cmd.append(f'{self.profile.repo.url}')
|
||||||
|
else:
|
||||||
|
cmd.append(f'{self.profile.repo.url}')
|
||||||
|
|
||||||
|
options = QFileDialog.Options()
|
||||||
|
options |= QFileDialog.ShowDirsOnly
|
||||||
|
options |= QFileDialog.DontUseNativeDialog
|
||||||
|
mountPoint = QFileDialog.getExistingDirectory(
|
||||||
|
self, "Choose Mount Point", "", options=options)
|
||||||
|
if mountPoint:
|
||||||
|
cmd.append(mountPoint)
|
||||||
|
|
||||||
|
self.set_status('Mounting snapshot into folder', 0)
|
||||||
|
thread = BorgThread(self, cmd, {})
|
||||||
|
thread.updated.connect(self.mount_update_log)
|
||||||
|
thread.result.connect(self.mount_get_result)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def mount_update_log(self, text):
|
||||||
|
self.mountErrors.setText(text)
|
||||||
|
|
||||||
|
def mount_get_result(self, result):
|
||||||
|
self.set_status(progress_max=100)
|
||||||
|
if result['returncode'] == 0:
|
||||||
|
self.set_status('Mounted successfully.')
|
||||||
|
|
||||||
def init_compression(self):
|
def init_compression(self):
|
||||||
self.repoCompression.addItem('LZ4 (default)', 'lz4')
|
self.repoCompression.addItem('LZ4 (default)', 'lz4')
|
||||||
self.repoCompression.addItem('Zstandard (medium)', 'zstd')
|
self.repoCompression.addItem('Zstandard (medium)', 'zstd')
|
||||||
self.repoCompression.addItem('LZMA (high)', 'lzma,6')
|
self.repoCompression.addItem('LZMA (high)', 'lzma,6')
|
||||||
|
self.repoCompression.setCurrentIndex(self.repoCompression.findData(self.profile.compression))
|
||||||
|
self.repoCompression.currentIndexChanged.connect(self.compression_select_action)
|
||||||
|
|
||||||
|
def compression_select_action(self, index):
|
||||||
|
self.profile.compression = self.repoCompression.currentData()
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
def init_repo(self):
|
||||||
|
self.repoSelector.model().item(0).setEnabled(False)
|
||||||
|
self.repoSelector.addItem('Initialize New Repository', 'init')
|
||||||
|
self.repoSelector.addItem('Add Existing Repository', 'existing')
|
||||||
|
for repo in RepoModel.select():
|
||||||
|
self.repoSelector.addItem(repo.url, repo.id)
|
||||||
|
|
||||||
|
if self.profile.repo:
|
||||||
|
self.repoSelector.setCurrentIndex(self.repoSelector.findData(self.profile.repo.id))
|
||||||
|
self.repoSelector.currentIndexChanged.connect(self.repo_select_action)
|
||||||
|
|
||||||
def repo_select_action(self, index):
|
def repo_select_action(self, index):
|
||||||
if index <= 2:
|
if index <= 2:
|
||||||
|
@ -132,40 +213,54 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
||||||
else:
|
else:
|
||||||
cmd = ["borg", "list", "--json", params['repo_url']]
|
cmd = ["borg", "list", "--json", params['repo_url']]
|
||||||
|
|
||||||
init_window = InitRepoWindow(cmd, params)
|
self.set_status('Connecting to repo...', 0)
|
||||||
init_window.setParent(self, QtCore.Qt.Sheet)
|
thread = BorgThread(self, cmd, params)
|
||||||
init_window.show()
|
thread.updated.connect(self.repo_add_update_log)
|
||||||
init_window._thread.start()
|
thread.result.connect(self.repo_add_result)
|
||||||
init_window._thread.result.connect(self.repo_add_result)
|
thread.start()
|
||||||
else:
|
else:
|
||||||
self.profile.repo = self.repoSelector.currentData()
|
self.profile.repo = self.repoSelector.currentData()
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
self.init_snapshots()
|
||||||
|
|
||||||
|
def repo_add_update_log(self, text):
|
||||||
|
self.set_status(text)
|
||||||
|
|
||||||
def repo_add_result(self, result):
|
def repo_add_result(self, result):
|
||||||
print(result)
|
|
||||||
if result['returncode'] == 0:
|
if result['returncode'] == 0:
|
||||||
|
self.set_status('Successfully connected to repo.', 100)
|
||||||
new_repo, _ = RepoModel.get_or_create(
|
new_repo, _ = RepoModel.get_or_create(
|
||||||
url=result['params']['repo_url'],
|
url=result['params']['repo_url'],
|
||||||
defaults={
|
defaults={
|
||||||
'password': result['params']['password'],
|
'password': result['params']['password'],
|
||||||
'encryption': 'na'
|
# 'encryption': result['params'].get('encryption', '')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if 'cache' in result['data']:
|
||||||
|
stats = result['data']['cache']['stats']
|
||||||
|
new_repo.total_size = stats['total_size']
|
||||||
|
new_repo.unique_csize = stats['unique_csize']
|
||||||
|
new_repo.unique_size = stats['unique_size']
|
||||||
|
new_repo.total_unique_chunks = stats['total_unique_chunks']
|
||||||
|
new_repo.encryption = result['data']['encryption']['mode']
|
||||||
new_repo.save()
|
new_repo.save()
|
||||||
|
self.profile.repo = new_repo.id
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
if 'archives' in result['data'].keys():
|
if 'archives' in result['data'].keys():
|
||||||
for snapshot in result['data']['archives']:
|
for snapshot in result['data']['archives']:
|
||||||
new_snapshot, _ = SnapshotModel.get_or_create(
|
new_snapshot, _ = SnapshotModel.get_or_create(
|
||||||
id=snapshot['id'],
|
snapshot_id=snapshot['id'],
|
||||||
defaults={
|
defaults={
|
||||||
'repo': new_repo.id,
|
'repo': new_repo.id,
|
||||||
'name': snapshot['name'],
|
'name': snapshot['name'],
|
||||||
'time': snapshot['time']
|
'time': parser.parse(snapshot['time'])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
new_snapshot.save()
|
new_snapshot.save()
|
||||||
self.repoSelector.addItem(new_repo.url, new_repo.id)
|
self.repoSelector.addItem(new_repo.url, new_repo.id)
|
||||||
self.repoSelector.setCurrentIndex(self.repoSelector.count()-1)
|
self.repoSelector.setCurrentIndex(self.repoSelector.count()-1)
|
||||||
|
self.init_snapshots()
|
||||||
|
|
||||||
def source_add(self):
|
def source_add(self):
|
||||||
options = QFileDialog.Options()
|
options = QFileDialog.Options()
|
||||||
|
|
|
@ -11,7 +11,11 @@ class RepoModel(peewee.Model):
|
||||||
url = peewee.CharField(unique=True)
|
url = peewee.CharField(unique=True)
|
||||||
password = peewee.CharField()
|
password = peewee.CharField()
|
||||||
added_at = peewee.DateTimeField(default=datetime.utcnow)
|
added_at = peewee.DateTimeField(default=datetime.utcnow)
|
||||||
encryption = peewee.CharField()
|
encryption = peewee.CharField(null=True)
|
||||||
|
unique_size = peewee.IntegerField(null=True)
|
||||||
|
unique_csize = peewee.IntegerField(null=True)
|
||||||
|
total_size = peewee.IntegerField(null=True)
|
||||||
|
total_unique_chunks = peewee.IntegerField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
@ -23,7 +27,7 @@ class BackupProfileModel(peewee.Model):
|
||||||
added_at = peewee.DateTimeField(default=datetime.utcnow)
|
added_at = peewee.DateTimeField(default=datetime.utcnow)
|
||||||
repo = peewee.ForeignKeyField(RepoModel, default=None, null=True)
|
repo = peewee.ForeignKeyField(RepoModel, default=None, null=True)
|
||||||
ssh_key = peewee.CharField(default=None, null=True)
|
ssh_key = peewee.CharField(default=None, null=True)
|
||||||
|
compression = peewee.CharField(default='lz4')
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
|
||||||
|
@ -40,11 +44,14 @@ class SourceDirModel(peewee.Model):
|
||||||
|
|
||||||
class SnapshotModel(peewee.Model):
|
class SnapshotModel(peewee.Model):
|
||||||
"""A snapshot to a specific remote repository."""
|
"""A snapshot to a specific remote repository."""
|
||||||
id = peewee.CharField(primary_key=True)
|
snapshot_id = peewee.CharField(unique=True)
|
||||||
name = peewee.CharField()
|
name = peewee.CharField()
|
||||||
repo = peewee.ForeignKeyField(RepoModel)
|
repo = peewee.ForeignKeyField(RepoModel, backref='snapshots')
|
||||||
time = peewee.DateTimeField()
|
time = peewee.DateTimeField()
|
||||||
|
|
||||||
|
def formatted_time(self):
|
||||||
|
return
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ class InitRepoWindow(InitRepoBase, InitRepoUI):
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.closeButton.clicked.connect(self.close)
|
self.closeButton.clicked.connect(self.close)
|
||||||
|
|
||||||
|
self.logText.appendPlainText('Connecting to repo...')
|
||||||
self._thread = BorgThread(self, cmd, env)
|
self._thread = BorgThread(self, cmd, env)
|
||||||
self._thread.updated.connect(self.update_log)
|
self._thread.updated.connect(self.update_log)
|
||||||
self._thread.result.connect(self.get_result)
|
self._thread.result.connect(self.get_result)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
def prettyByes(size):
|
||||||
|
"""from https://stackoverflow.com/questions/12523586/
|
||||||
|
python-format-size-application-converting-b-to-kb-mb-gb-tb/37423778"""
|
||||||
|
if type(size) != int:
|
||||||
|
return ''
|
||||||
|
power = 2**10
|
||||||
|
n = 0
|
||||||
|
Dic_powerN = {0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'}
|
||||||
|
while size > power:
|
||||||
|
size /= power
|
||||||
|
n += 1
|
||||||
|
return str(round(size))+Dic_powerN[n]+'B'
|
Loading…
Reference in New Issue