Working version with snapshots and ssh creation.

This commit is contained in:
Manu 2018-10-27 18:12:30 +08:00
parent 8cede50bd1
commit 1d0a6b5f45
8 changed files with 249 additions and 106 deletions

View File

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

View File

@ -12,6 +12,7 @@ dependencies:
- paramiko - paramiko
- pyqt5 - pyqt5
- peewee - peewee
- python-dateutil
dev-dependencies: dev-dependencies:
- pyinstaller - pyinstaller

View File

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

View File

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

View File

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

View File

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

View File

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

13
vorta/utils.py Normal file
View File

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