From 3a5eafd06137b3e45788cae9a2e356e654d457ff Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 2 Nov 2018 00:53:41 +0800 Subject: [PATCH] Replace peewee_migrate with native solution. Fix window activation. --- .gitignore | 1 + README.md | 14 +- setup.cfg | 5 +- src/vorta/__main__.py | 6 - src/vorta/application.py | 4 +- src/vorta/assets/UI/mainwindow.ui | 7 +- src/vorta/assets/UI/repotab.ui | 2 +- src/vorta/assets/UI/snapshottab.ui | 10 + src/vorta/config.py | 4 - src/vorta/migrations/001_add-snapshot-size.py | 38 ---- src/vorta/migrations/__init__.py | 0 src/vorta/models.py | 159 ++++++++++------ src/vorta/tray_menu.py | 10 +- src/vorta/utils.py | 2 +- src/vorta/views/collection_rc.py | 174 +++++++++--------- src/vorta/views/repo_tab.py | 8 +- src/vorta/views/snapshots_tab.py | 18 +- tests/test_schedule.py | 6 +- 18 files changed, 246 insertions(+), 222 deletions(-) delete mode 100644 src/vorta/migrations/001_add-snapshot-size.py delete mode 100644 src/vorta/migrations/__init__.py 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') -