diff --git a/.gitignore b/.gitignore
index 15d048e5..f4e9d38e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ __pycache__
Makefile
.eggs
vorta.egg-info
+.coverage
diff --git a/README.md b/README.md
index 999b8b93..ff863d1e 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,9 @@ Missing features: (PRs welcome)
- [ ] Repo pruning
- [ ] Repo checking
-- [ ] Full test coverage
-- [ ] Use static type checks via mypy
-- [ ] Packaging for Linux
+- [ ] Full test coverage (currently: 65%)
+- [ ] Use static type checks via mypy?
+- [ ] Packaging for Linux? How?
## Installation and Download
### macOS
@@ -55,8 +55,6 @@ $ brew cask install qt-creator
$ brew install qt
```
-[Peewee Migrate](https://github.com/klen/peewee_migrate) is used to manage database migrations. Add them to the `vorta.migrations` package.
-
To build a binary package:
```
$ pyinstaller --clean --noconfirm vorta.spec
@@ -68,6 +66,12 @@ Tests are in the folder `/tests`. Testing happens at the level of UI components.
$ pytest
```
+To update and view coverage information
+```
+$ coverage run -m pytest
+$ coverage report
+```
+
## Privacy Policy
- No personal data is ever stored or transmitted by this application.
- During beta, crash reports are sent to [Sentry](https://sentry.io) to quickly find bugs.
diff --git a/setup.cfg b/setup.cfg
index 206e63f2..5c59dc07 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,7 +41,6 @@ install_requires =
python-dateutil
keyring
borgbackup
- peewee_migrate
[options.extras_require]
tests =
@@ -51,6 +50,7 @@ tests =
pytest-faulthandler
mypy
mypy-extensions
+ coverage
[options.entry_points]
gui_scripts =
@@ -64,4 +64,7 @@ qt_default_raising = true
filterwarnings =
ignore::DeprecationWarning
+[coverage:run]
+source = src
+
[tox:tox]
diff --git a/src/vorta/__main__.py b/src/vorta/__main__.py
index f21c6c77..742470ab 100644
--- a/src/vorta/__main__.py
+++ b/src/vorta/__main__.py
@@ -3,7 +3,6 @@
import peewee
import vorta.models
-import vorta.migrations
from vorta.application import VortaApp
from vorta.config import SETTINGS_DIR
@@ -16,10 +15,5 @@
sqlite_db = peewee.SqliteDatabase(os.path.join(SETTINGS_DIR, 'settings.db'))
vorta.models.init_db(sqlite_db)
-# Run migrations
-from peewee_migrate.cli import get_router
-router = get_router(os.path.dirname(vorta.migrations.__file__), sqlite_db, True)
-router.run()
-
app = VortaApp(sys.argv)
sys.exit(app.exec_())
diff --git a/src/vorta/application.py b/src/vorta/application.py
index 73e83a28..baadc11c 100644
--- a/src/vorta/application.py
+++ b/src/vorta/application.py
@@ -1,5 +1,5 @@
import sys
-
+import os
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication
@@ -47,8 +47,8 @@ def create_backup(self):
return msg
def on_open_main_window(self):
- self.main_window = MainWindow(self)
self.main_window.show()
+ self.main_window.raise_()
def create_backup_result(self, result):
self.backup_done.emit()
diff --git a/src/vorta/assets/UI/mainwindow.ui b/src/vorta/assets/UI/mainwindow.ui
index efc95f5e..6a57905c 100644
--- a/src/vorta/assets/UI/mainwindow.ui
+++ b/src/vorta/assets/UI/mainwindow.ui
@@ -35,7 +35,7 @@
0
0
801
- 561
+ 571
@@ -154,6 +154,11 @@
-
+
+
+ 11
+
+
diff --git a/src/vorta/assets/UI/repotab.ui b/src/vorta/assets/UI/repotab.ui
index 2f83ba87..77e63eec 100644
--- a/src/vorta/assets/UI/repotab.ui
+++ b/src/vorta/assets/UI/repotab.ui
@@ -81,7 +81,7 @@
-
- <html><head/><body><p>SSH account with Borg installed server-side. Also try <a href="https://www.borgbase.com"><span style=" text-decoration: underline; color:#0000ff;">BorgBase.</span></a> 100GB free during Beta.</p></body></html>
+ <html><head/><body><p>SSH account with Borg installed server-side. Also try <a href="https://www.borgbase.com?utm_source=vorta"><span style=" text-decoration: underline; color:#0000ff;">BorgBase</span></a>. 100GB free during Beta.</p></body></html>
true
diff --git a/src/vorta/assets/UI/snapshottab.ui b/src/vorta/assets/UI/snapshottab.ui
index d44f3d88..2c3d784f 100644
--- a/src/vorta/assets/UI/snapshottab.ui
+++ b/src/vorta/assets/UI/snapshottab.ui
@@ -47,6 +47,16 @@
Name
+
+
+ Size
+
+
+
+
+ Duration
+
+
Date
diff --git a/src/vorta/config.py b/src/vorta/config.py
index 6db8b9b3..af659aee 100644
--- a/src/vorta/config.py
+++ b/src/vorta/config.py
@@ -1,6 +1,5 @@
import appdirs
import os
-import shutil
APP_NAME = 'Vorta'
APP_AUTHOR = 'BorgBase'
@@ -8,6 +7,3 @@
if not os.path.exists(SETTINGS_DIR):
os.makedirs(SETTINGS_DIR)
-
-def remove_config():
- shutil.rmtree(SETTINGS_DIR)
diff --git a/src/vorta/migrations/001_add-snapshot-size.py b/src/vorta/migrations/001_add-snapshot-size.py
deleted file mode 100644
index 9e1e3c70..00000000
--- a/src/vorta/migrations/001_add-snapshot-size.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""Peewee migrations -- 001_add-snapshot-size.py.
-
-Some examples (model - class or model name)::
-
- > Model = migrator.orm['model_name'] # Return model in current state by name
-
- > migrator.sql(sql) # Run custom SQL
- > migrator.python(func, *args, **kwargs) # Run python code
- > migrator.create_model(Model) # Create a model (could be used as decorator)
- > migrator.remove_model(model, cascade=True) # Remove a model
- > migrator.add_fields(model, **fields) # Add fields to a model
- > migrator.change_fields(model, **fields) # Change fields
- > migrator.remove_fields(model, *field_names, cascade=True)
- > migrator.rename_field(model, old_field_name, new_field_name)
- > migrator.rename_table(model, new_table_name)
- > migrator.add_index(model, *col_names, unique=False)
- > migrator.drop_index(model, *col_names)
- > migrator.add_not_null(model, *field_names)
- > migrator.drop_not_null(model, *field_names)
- > migrator.add_default(model, field_name, default)
-
-"""
-
-import peewee as pw
-from vorta.models import *
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
- migrator.add_fields(SnapshotModel, duration=pw.FloatField(null=True))
- migrator.add_fields(SnapshotModel, size=pw.IntegerField(null=True))
-
-
-
-def rollback(migrator, database, fake=False, **kwargs):
- """Write your rollback migrations here."""
-
diff --git a/src/vorta/migrations/__init__.py b/src/vorta/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/vorta/models.py b/src/vorta/models.py
index 688fdffa..c12b96f8 100644
--- a/src/vorta/models.py
+++ b/src/vorta/models.py
@@ -1,11 +1,20 @@
-import peewee
+"""
+This module provides the app's data store using Peewee with SQLite.
+
+At the bottom there is a simple schema migration system.
+"""
+
+import peewee as pw
import json
from datetime import datetime
+from playhouse.migrate import SqliteMigrator, migrate
-db = peewee.Proxy()
+SCHEMA_VERSION = 4
+
+db = pw.Proxy()
-class JSONField(peewee.TextField):
+class JSONField(pw.TextField):
"""
Class to "fake" a JSON field with a text field. Not efficient but works nicely
@@ -21,65 +30,65 @@ def python_value(self, value):
-class RepoModel(peewee.Model):
+class RepoModel(pw.Model):
"""A single remote repo with unique URL."""
- url = peewee.CharField(unique=True)
- added_at = peewee.DateTimeField(default=datetime.utcnow)
- 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)
+ url = pw.CharField(unique=True)
+ added_at = pw.DateTimeField(default=datetime.utcnow)
+ encryption = pw.CharField(null=True)
+ unique_size = pw.IntegerField(null=True)
+ unique_csize = pw.IntegerField(null=True)
+ total_size = pw.IntegerField(null=True)
+ total_unique_chunks = pw.IntegerField(null=True)
class Meta:
database = db
-class BackupProfileModel(peewee.Model):
+class BackupProfileModel(pw.Model):
"""Allows the user to switch between different configurations."""
- name = peewee.CharField()
- added_at = peewee.DateTimeField(default=datetime.now)
- repo = peewee.ForeignKeyField(RepoModel, default=None, null=True)
- ssh_key = peewee.CharField(default=None, null=True)
- compression = peewee.CharField(default='lz4')
- exclude_patterns = peewee.TextField(null=True)
- exclude_if_present = peewee.TextField(null=True)
- schedule_mode = peewee.CharField(default='off')
- schedule_interval_hours = peewee.IntegerField(default=3)
- schedule_interval_minutes = peewee.IntegerField(default=42)
- schedule_fixed_hour = peewee.IntegerField(default=3)
- schedule_fixed_minute = peewee.IntegerField(default=42)
- validation_on = peewee.BooleanField(default=True)
- validation_weeks = peewee.IntegerField(default=3)
- prune_on = peewee.BooleanField(default=False)
- prune_hour = peewee.IntegerField(default=2)
- prune_day = peewee.IntegerField(default=7)
- prune_week = peewee.IntegerField(default=4)
- prune_month = peewee.IntegerField(default=6)
- prune_year = peewee.IntegerField(default=2)
+ name = pw.CharField()
+ added_at = pw.DateTimeField(default=datetime.now)
+ repo = pw.ForeignKeyField(RepoModel, default=None, null=True)
+ ssh_key = pw.CharField(default=None, null=True)
+ compression = pw.CharField(default='lz4')
+ exclude_patterns = pw.TextField(null=True)
+ exclude_if_present = pw.TextField(null=True)
+ schedule_mode = pw.CharField(default='off')
+ schedule_interval_hours = pw.IntegerField(default=3)
+ schedule_interval_minutes = pw.IntegerField(default=42)
+ schedule_fixed_hour = pw.IntegerField(default=3)
+ schedule_fixed_minute = pw.IntegerField(default=42)
+ validation_on = pw.BooleanField(default=True)
+ validation_weeks = pw.IntegerField(default=3)
+ prune_on = pw.BooleanField(default=False)
+ prune_hour = pw.IntegerField(default=2)
+ prune_day = pw.IntegerField(default=7)
+ prune_week = pw.IntegerField(default=4)
+ prune_month = pw.IntegerField(default=6)
+ prune_year = pw.IntegerField(default=2)
class Meta:
database = db
-class SourceDirModel(peewee.Model):
+class SourceDirModel(pw.Model):
"""A folder to be backed up, related to a Backup Configuration."""
- dir = peewee.CharField()
- config = peewee.ForeignKeyField(BackupProfileModel, default=1)
- added_at = peewee.DateTimeField(default=datetime.utcnow)
+ dir = pw.CharField()
+ config = pw.ForeignKeyField(BackupProfileModel, default=1)
+ added_at = pw.DateTimeField(default=datetime.utcnow)
class Meta:
database = db
-class SnapshotModel(peewee.Model):
+class SnapshotModel(pw.Model):
"""A snapshot to a specific remote repository."""
- snapshot_id = peewee.CharField(unique=True)
- name = peewee.CharField()
- repo = peewee.ForeignKeyField(RepoModel, backref='snapshots')
- time = peewee.DateTimeField()
- duration = peewee.FloatField(null=True)
- size = peewee.IntegerField(null=True)
+ snapshot_id = pw.CharField(unique=True)
+ name = pw.CharField()
+ repo = pw.ForeignKeyField(RepoModel, backref='snapshots')
+ time = pw.DateTimeField()
+ duration = pw.FloatField(null=True)
+ size = pw.IntegerField(null=True)
def formatted_time(self):
return
@@ -88,26 +97,35 @@ class Meta:
database = db
-class WifiSettingModel(peewee.Model):
+class WifiSettingModel(pw.Model):
"""Save Wifi Settings"""
- ssid = peewee.CharField()
- last_connected = peewee.DateTimeField()
- allowed = peewee.BooleanField(default=True)
- profile = peewee.ForeignKeyField(BackupProfileModel, default=1)
+ ssid = pw.CharField()
+ last_connected = pw.DateTimeField()
+ allowed = pw.BooleanField(default=True)
+ profile = pw.ForeignKeyField(BackupProfileModel, default=1)
class Meta:
database = db
-class EventLogModel(peewee.Model):
+class EventLogModel(pw.Model):
"""Keep a log of background jobs."""
- start_time = peewee.DateTimeField(default=datetime.now)
- category = peewee.CharField()
- subcommand = peewee.CharField(null=True)
- message = peewee.CharField(null=True)
- returncode = peewee.IntegerField(default=1)
+ start_time = pw.DateTimeField(default=datetime.now)
+ category = pw.CharField()
+ subcommand = pw.CharField(null=True)
+ message = pw.CharField(null=True)
+ returncode = pw.IntegerField(default=1)
params = JSONField(null=True)
- profile = peewee.ForeignKeyField(BackupProfileModel, default=1)
+ profile = pw.ForeignKeyField(BackupProfileModel, default=1)
+
+ class Meta:
+ database = db
+
+
+class SchemaVersion(pw.Model):
+ """Keep DB version to apply the correct migrations."""
+ version = pw.IntegerField()
+ changed_at = pw.DateTimeField(default=datetime.now)
class Meta:
database = db
@@ -115,16 +133,43 @@ class Meta:
class BackupProfileMixin:
"""Extend to support multiple profiles later."""
-
@property
def profile(self):
return BackupProfileModel.get(id=1)
+def _apply_schema_update(current_schema, version_after, *operations):
+ with db.atomic():
+ migrate(*operations)
+ current_schema.version = version_after
+ current_schema.changed_at = datetime.now()
+ current_schema.save()
+
+
def init_db(con):
db.initialize(con)
db.connect()
db.create_tables([RepoModel, BackupProfileModel, SourceDirModel,
- SnapshotModel, WifiSettingModel, EventLogModel])
+ SnapshotModel, WifiSettingModel, EventLogModel, SchemaVersion])
BackupProfileModel.get_or_create(id=1, name='Default')
+
+ # Migrations
+ # See http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations
+ current_schema, created = SchemaVersion.get_or_create(id=1, defaults={'version': SCHEMA_VERSION})
+ current_schema.save()
+ if created or current_schema.version == SCHEMA_VERSION:
+ return
+ else:
+ migrator = SqliteMigrator(con)
+
+ if current_schema.version < 3: # version 2 to 3
+ pass
+
+ if current_schema.version < 4: # version 3 to 4
+ _apply_schema_update(
+ current_schema, 4,
+ migrator.add_column(SnapshotModel._meta.table_name, 'duration', pw.FloatField(null=True)),
+ migrator.add_column(SnapshotModel._meta.table_name, 'size', pw.IntegerField(null=True))
+ )
+
diff --git a/src/vorta/tray_menu.py b/src/vorta/tray_menu.py
index f9075515..49d0ee35 100644
--- a/src/vorta/tray_menu.py
+++ b/src/vorta/tray_menu.py
@@ -4,7 +4,6 @@
from PyQt5.QtGui import QIcon
from .utils import get_asset
-from .config import remove_config
from .borg_runner import BorgThread
@@ -29,9 +28,6 @@ def __init__(self, parent=None):
menu.addSeparator()
- exit_action = menu.addAction("Factory Reset")
- exit_action.triggered.connect(self.on_reset)
-
exit_action = menu.addAction("Exit")
exit_action.triggered.connect(self.on_exit_action)
@@ -44,15 +40,11 @@ def __init__(self, parent=None):
def on_exit_action(self):
self.app.quit()
- def on_reset(self):
- remove_config()
- self.app.quit()
-
def on_user_click(self):
"""Adjust labels to reflect current status."""
if BorgThread.is_running():
self.status.setText('Backup in Progress')
self.create_action.setText('Cancel Backup')
else:
- self.status.setText(self.app.scheduler.next_job)
+ self.status.setText(f'Next Task: {self.app.scheduler.next_job}')
self.create_action.setText('Backup Now')
diff --git a/src/vorta/utils.py b/src/vorta/utils.py
index 553fb21b..22f5031f 100644
--- a/src/vorta/utils.py
+++ b/src/vorta/utils.py
@@ -49,7 +49,7 @@ def get_private_keys():
return available_private_keys
-def prettyBytes(size):
+def pretty_bytes(size):
"""from https://stackoverflow.com/questions/12523586/
python-format-size-application-converting-b-to-kb-mb-gb-tb/37423778"""
if type(size) != int:
diff --git a/src/vorta/views/collection_rc.py b/src/vorta/views/collection_rc.py
index 2da011a6..f1b2baea 100644
--- a/src/vorta/views/collection_rc.py
+++ b/src/vorta/views/collection_rc.py
@@ -9,6 +9,42 @@
from PyQt5 import QtCore
qt_resource_data = b"\
+\x00\x00\x02\x12\
+\x3c\
+\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
+\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\
+\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\
+\x3d\x22\x31\x37\x39\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\
+\x31\x37\x39\x32\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\
+\x20\x30\x20\x31\x37\x39\x32\x20\x31\x37\x39\x32\x22\x20\x78\x6d\
+\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\
+\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\
+\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x30\x32\x34\x20\
+\x31\x34\x30\x38\x68\x36\x34\x30\x76\x2d\x31\x32\x38\x68\x2d\x36\
+\x34\x30\x76\x31\x32\x38\x7a\x6d\x2d\x33\x38\x34\x2d\x35\x31\x32\
+\x68\x31\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\
+\x76\x31\x32\x38\x7a\x6d\x36\x34\x30\x2d\x35\x31\x32\x68\x33\x38\
+\x34\x76\x2d\x31\x32\x38\x68\x2d\x33\x38\x34\x76\x31\x32\x38\x7a\
+\x6d\x35\x31\x32\x20\x38\x33\x32\x76\x32\x35\x36\x71\x30\x20\x32\
+\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\x31\x39\x68\x2d\
+\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\x35\x2d\x31\x39\
+\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\x71\x30\x2d\x32\
+\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\x39\x68\x31\x36\
+\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\x39\x74\x31\x39\
+\x20\x34\x35\x7a\x6d\x30\x2d\x35\x31\x32\x76\x32\x35\x36\x71\x30\
+\x20\x32\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\x31\x39\
+\x68\x2d\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\x35\x2d\
+\x31\x39\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\x71\x30\
+\x2d\x32\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\x39\x68\
+\x31\x36\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\x39\x74\
+\x31\x39\x20\x34\x35\x7a\x6d\x30\x2d\x35\x31\x32\x76\x32\x35\x36\
+\x71\x30\x20\x32\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\
+\x31\x39\x68\x2d\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\
+\x35\x2d\x31\x39\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\
+\x71\x30\x2d\x32\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\
+\x39\x68\x31\x36\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\
+\x39\x74\x31\x39\x20\x34\x35\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\
+\x3e\
\x00\x00\x01\x89\
\x3c\
\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
@@ -46,40 +82,6 @@
\x20\x30\x20\x31\x37\x39\x32\x20\x31\x37\x39\x32\x22\x20\x78\x6d\
\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\
\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\
-\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x32\x38\x20\x31\
-\x34\x30\x38\x68\x31\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\
-\x30\x32\x34\x76\x31\x32\x38\x7a\x6d\x30\x2d\x35\x31\x32\x68\x31\
-\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\x76\x31\
-\x32\x38\x7a\x6d\x31\x35\x36\x38\x20\x34\x34\x38\x71\x30\x2d\x34\
-\x30\x2d\x32\x38\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\
-\x38\x20\x32\x38\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\
-\x20\x36\x38\x20\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\
-\x36\x38\x7a\x6d\x2d\x31\x35\x36\x38\x2d\x39\x36\x30\x68\x31\x30\
-\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\x76\x31\x32\
-\x38\x7a\x6d\x31\x35\x36\x38\x20\x34\x34\x38\x71\x30\x2d\x34\x30\
-\x2d\x32\x38\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\x38\
-\x20\x32\x38\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\x20\
-\x36\x38\x20\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\x36\
-\x38\x7a\x6d\x30\x2d\x35\x31\x32\x71\x30\x2d\x34\x30\x2d\x32\x38\
-\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\x38\x20\x32\x38\
-\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\x20\x36\x38\x20\
-\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\x36\x38\x7a\x6d\
-\x39\x36\x20\x38\x33\x32\x76\x33\x38\x34\x68\x2d\x31\x37\x39\x32\
-\x76\x2d\x33\x38\x34\x68\x31\x37\x39\x32\x7a\x6d\x30\x2d\x35\x31\
-\x32\x76\x33\x38\x34\x68\x2d\x31\x37\x39\x32\x76\x2d\x33\x38\x34\
-\x68\x31\x37\x39\x32\x7a\x6d\x30\x2d\x35\x31\x32\x76\x33\x38\x34\
-\x68\x2d\x31\x37\x39\x32\x76\x2d\x33\x38\x34\x68\x31\x37\x39\x32\
-\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\
-\x00\x00\x01\xfb\
-\x3c\
-\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
-\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\
-\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\
-\x3d\x22\x31\x37\x39\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\
-\x31\x37\x39\x32\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\
-\x20\x30\x20\x31\x37\x39\x32\x20\x31\x37\x39\x32\x22\x20\x78\x6d\
-\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\
-\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\
\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x30\x32\x34\x20\
\x35\x34\x34\x76\x34\x34\x38\x71\x30\x20\x31\x34\x2d\x39\x20\x32\
\x33\x74\x2d\x32\x33\x20\x39\x68\x2d\x33\x32\x30\x71\x2d\x31\x34\
@@ -104,6 +106,40 @@
\x20\x33\x38\x35\x2e\x35\x20\x31\x30\x33\x20\x32\x37\x39\x2e\x35\
\x20\x32\x37\x39\x2e\x35\x20\x31\x30\x33\x20\x33\x38\x35\x2e\x35\
\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\
+\x00\x00\x01\xfb\
+\x3c\
+\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
+\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\
+\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\
+\x3d\x22\x31\x37\x39\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\
+\x31\x37\x39\x32\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\
+\x20\x30\x20\x31\x37\x39\x32\x20\x31\x37\x39\x32\x22\x20\x78\x6d\
+\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\
+\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\
+\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x32\x38\x20\x31\
+\x34\x30\x38\x68\x31\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\
+\x30\x32\x34\x76\x31\x32\x38\x7a\x6d\x30\x2d\x35\x31\x32\x68\x31\
+\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\x76\x31\
+\x32\x38\x7a\x6d\x31\x35\x36\x38\x20\x34\x34\x38\x71\x30\x2d\x34\
+\x30\x2d\x32\x38\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\
+\x38\x20\x32\x38\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\
+\x20\x36\x38\x20\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\
+\x36\x38\x7a\x6d\x2d\x31\x35\x36\x38\x2d\x39\x36\x30\x68\x31\x30\
+\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\x76\x31\x32\
+\x38\x7a\x6d\x31\x35\x36\x38\x20\x34\x34\x38\x71\x30\x2d\x34\x30\
+\x2d\x32\x38\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\x38\
+\x20\x32\x38\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\x20\
+\x36\x38\x20\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\x36\
+\x38\x7a\x6d\x30\x2d\x35\x31\x32\x71\x30\x2d\x34\x30\x2d\x32\x38\
+\x2d\x36\x38\x74\x2d\x36\x38\x2d\x32\x38\x2d\x36\x38\x20\x32\x38\
+\x2d\x32\x38\x20\x36\x38\x20\x32\x38\x20\x36\x38\x20\x36\x38\x20\
+\x32\x38\x20\x36\x38\x2d\x32\x38\x20\x32\x38\x2d\x36\x38\x7a\x6d\
+\x39\x36\x20\x38\x33\x32\x76\x33\x38\x34\x68\x2d\x31\x37\x39\x32\
+\x76\x2d\x33\x38\x34\x68\x31\x37\x39\x32\x7a\x6d\x30\x2d\x35\x31\
+\x32\x76\x33\x38\x34\x68\x2d\x31\x37\x39\x32\x76\x2d\x33\x38\x34\
+\x68\x31\x37\x39\x32\x7a\x6d\x30\x2d\x35\x31\x32\x76\x33\x38\x34\
+\x68\x2d\x31\x37\x39\x32\x76\x2d\x33\x38\x34\x68\x31\x37\x39\x32\
+\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\
\x00\x00\x03\x19\
\x3c\
\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
@@ -156,42 +192,6 @@
\x31\x30\x20\x31\x30\x20\x32\x33\x20\x30\x20\x31\x38\x2d\x37\x35\
\x2e\x35\x20\x39\x33\x74\x2d\x39\x32\x2e\x35\x20\x37\x35\x7a\x22\
\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\
-\x00\x00\x02\x12\
-\x3c\
-\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\
-\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\
-\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\
-\x3d\x22\x31\x37\x39\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\
-\x31\x37\x39\x32\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\
-\x20\x30\x20\x31\x37\x39\x32\x20\x31\x37\x39\x32\x22\x20\x78\x6d\
-\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\
-\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\
-\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x30\x32\x34\x20\
-\x31\x34\x30\x38\x68\x36\x34\x30\x76\x2d\x31\x32\x38\x68\x2d\x36\
-\x34\x30\x76\x31\x32\x38\x7a\x6d\x2d\x33\x38\x34\x2d\x35\x31\x32\
-\x68\x31\x30\x32\x34\x76\x2d\x31\x32\x38\x68\x2d\x31\x30\x32\x34\
-\x76\x31\x32\x38\x7a\x6d\x36\x34\x30\x2d\x35\x31\x32\x68\x33\x38\
-\x34\x76\x2d\x31\x32\x38\x68\x2d\x33\x38\x34\x76\x31\x32\x38\x7a\
-\x6d\x35\x31\x32\x20\x38\x33\x32\x76\x32\x35\x36\x71\x30\x20\x32\
-\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\x31\x39\x68\x2d\
-\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\x35\x2d\x31\x39\
-\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\x71\x30\x2d\x32\
-\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\x39\x68\x31\x36\
-\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\x39\x74\x31\x39\
-\x20\x34\x35\x7a\x6d\x30\x2d\x35\x31\x32\x76\x32\x35\x36\x71\x30\
-\x20\x32\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\x31\x39\
-\x68\x2d\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\x35\x2d\
-\x31\x39\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\x71\x30\
-\x2d\x32\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\x39\x68\
-\x31\x36\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\x39\x74\
-\x31\x39\x20\x34\x35\x7a\x6d\x30\x2d\x35\x31\x32\x76\x32\x35\x36\
-\x71\x30\x20\x32\x36\x2d\x31\x39\x20\x34\x35\x74\x2d\x34\x35\x20\
-\x31\x39\x68\x2d\x31\x36\x36\x34\x71\x2d\x32\x36\x20\x30\x2d\x34\
-\x35\x2d\x31\x39\x74\x2d\x31\x39\x2d\x34\x35\x76\x2d\x32\x35\x36\
-\x71\x30\x2d\x32\x36\x20\x31\x39\x2d\x34\x35\x74\x34\x35\x2d\x31\
-\x39\x68\x31\x36\x36\x34\x71\x32\x36\x20\x30\x20\x34\x35\x20\x31\
-\x39\x74\x31\x39\x20\x34\x35\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\
-\x3e\
"
qt_resource_name = b"\
@@ -199,37 +199,37 @@
\x00\x6f\xa6\x53\
\x00\x69\
\x00\x63\x00\x6f\x00\x6e\x00\x73\
+\x00\x09\
+\x0a\x26\xaf\xc7\
+\x00\x74\
+\x00\x61\x00\x73\x00\x6b\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x12\
\x05\x98\xe2\x07\
\x00\x77\
\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x2d\x00\x72\x00\x65\x00\x73\x00\x74\x00\x6f\x00\x72\x00\x65\x00\x2e\x00\x73\x00\x76\
\x00\x67\
-\x00\x0a\
-\x0c\xca\x63\xe7\
-\x00\x73\
-\x00\x65\x00\x72\x00\x76\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x0b\
\x0f\x16\x31\xe7\
\x00\x63\
\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x2d\x00\x6f\x00\x2e\x00\x73\x00\x76\x00\x67\
+\x00\x0a\
+\x0c\xca\x63\xe7\
+\x00\x73\
+\x00\x65\x00\x72\x00\x76\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\
\x00\x08\
\x0f\xcc\x55\x67\
\x00\x77\
\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\
-\x00\x09\
-\x0a\x26\xaf\xc7\
-\x00\x74\
-\x00\x61\x00\x73\x00\x6b\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\
"
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\
+\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x02\x16\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x08\xa8\
-\x00\x00\x00\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x01\x8d\
-\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x03\x8c\
-\x00\x00\x00\x70\x00\x00\x00\x00\x00\x01\x00\x00\x05\x8b\
+\x00\x00\x00\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x05\xa2\
+\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x03\xa3\
+\x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x07\xa1\
"
qt_resource_struct_v2 = b"\
@@ -237,15 +237,15 @@
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x02\x16\
\x00\x00\x01\x66\xbf\xa1\xb1\x2c\
-\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x08\xa8\
+\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x66\xbf\x9f\xde\x3e\
-\x00\x00\x00\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x01\x8d\
+\x00\x00\x00\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x05\xa2\
\x00\x00\x01\x66\xbf\xa1\x0f\xda\
-\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x03\x8c\
+\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x03\xa3\
\x00\x00\x01\x66\xbf\x9d\x97\x00\
-\x00\x00\x00\x70\x00\x00\x00\x00\x00\x01\x00\x00\x05\x8b\
+\x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x07\xa1\
\x00\x00\x01\x66\xbf\x9f\x19\x44\
"
diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py
index 0153feb7..19b59435 100644
--- a/src/vorta/views/repo_tab.py
+++ b/src/vorta/views/repo_tab.py
@@ -3,7 +3,7 @@
from ..models import RepoModel, SnapshotModel, BackupProfileMixin
from .repo_add import AddRepoWindow, ExistingRepoWindow
-from ..utils import prettyBytes, get_private_keys, get_asset, keyring
+from ..utils import pretty_bytes, get_private_keys, get_asset, keyring
from .ssh_add import SSHAddWindow
uifile = get_asset('UI/repotab.ui')
@@ -40,9 +40,9 @@ def __init__(self, parent=None):
def init_repo_stats(self):
repo = self.profile.repo
- self.sizeCompressed.setText(prettyBytes(repo.unique_csize))
- self.sizeDeduplicated.setText(prettyBytes(repo.unique_size))
- self.sizeOriginal.setText(prettyBytes(repo.total_size))
+ self.sizeCompressed.setText(pretty_bytes(repo.unique_csize))
+ self.sizeDeduplicated.setText(pretty_bytes(repo.unique_size))
+ self.sizeOriginal.setText(pretty_bytes(repo.total_size))
self.repoEncryption.setText(str(repo.encryption))
self.repo_changed.emit(repo.id)
diff --git a/src/vorta/views/snapshots_tab.py b/src/vorta/views/snapshots_tab.py
index 4d5490e1..d5349005 100644
--- a/src/vorta/views/snapshots_tab.py
+++ b/src/vorta/views/snapshots_tab.py
@@ -1,8 +1,9 @@
+from datetime import timedelta
from PyQt5 import uic
from PyQt5.QtWidgets import QFileDialog, QTableWidgetItem, QTableView, QHeaderView
from ..borg_runner import BorgThread
-from ..utils import get_asset, keyring
+from ..utils import get_asset, keyring, pretty_bytes
from ..models import BackupProfileMixin
uifile = get_asset('UI/snapshottab.ui')
@@ -18,6 +19,8 @@ def __init__(self, parent=None):
header.setVisible(True)
header.setSectionResizeMode(0, QHeaderView.Stretch)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
+ header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
+ header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
self.snapshotTable.setSelectionBehavior(QTableView.SelectRows)
self.snapshotTable.setEditTriggers(QTableView.NoEditTriggers)
@@ -39,8 +42,14 @@ def populate(self):
for row, snapshot in enumerate(snapshots):
self.snapshotTable.insertRow(row)
self.snapshotTable.setItem(row, 0, QTableWidgetItem(snapshot.name))
+ self.snapshotTable.setItem(row, 1, QTableWidgetItem(pretty_bytes(snapshot.size)))
+ if snapshot.duration:
+ formatted_duration = str(timedelta(seconds=round(snapshot.duration)))
+ else:
+ formatted_duration = 'N/A'
+ self.snapshotTable.setItem(row, 2, QTableWidgetItem(formatted_duration))
formatted_time = snapshot.time.strftime('%Y-%m-%d %H:%M')
- self.snapshotTable.setItem(row, 1, QTableWidgetItem(formatted_time))
+ self.snapshotTable.setItem(row, 3, QTableWidgetItem(formatted_time))
self.snapshotTable.setRowCount(len(snapshots))
def snapshot_mount(self):
@@ -67,7 +76,8 @@ def snapshot_mount(self):
self.set_status('Mounting snapshot into folder...')
params = {'password': keyring.get_password("vorta-repo", profile.repo.url)}
- thread = BorgThread(self, cmd, params, parent=self)
+ self.snapshotMountButton.setEnabled(False)
+ thread = BorgThread(cmd, params, parent=self)
thread.updated.connect(self.mount_update_log)
thread.result.connect(self.mount_get_result)
thread.start()
@@ -76,5 +86,7 @@ def mount_update_log(self, text):
self.mountErrors.setText(text)
def mount_get_result(self, result):
+ self.snapshotMountButton.setEnabled(True)
if result['returncode'] == 0:
self.set_status('Mounted successfully.')
+
diff --git a/tests/test_schedule.py b/tests/test_schedule.py
index 2aff3b52..00a0b1f5 100644
--- a/tests/test_schedule.py
+++ b/tests/test_schedule.py
@@ -1,13 +1,14 @@
+import pytest
from datetime import datetime as dt, date, time
from PyQt5 import QtCore
from vorta.views.schedule_tab import ScheduleTab
-from .fixtures import app, main
+from .fixtures import *
def test_schedule_tab(main, qtbot):
tab = ScheduleTab(main.scheduleTabSlot)
- qtbot.addWidget(tab)
+ # qtbot.addWidget(tab)
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
assert tab.nextBackupDateTimeLabel.text() == 'Manual Backups'
@@ -26,4 +27,3 @@ def test_schedule_tab(main, qtbot):
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
next_backup = dt.combine(date.today(), time(23, 59))
assert tab.nextBackupDateTimeLabel.text() == next_backup.strftime('%Y-%m-%d %H:%M')
-