mirror of https://github.com/borgbase/vorta
Remove APScheduler dependency, Python 3.10 support (#1086)
This commit is contained in:
parent
c963aaf3f2
commit
c7a1077fec
|
@ -18,7 +18,7 @@ jobs:
|
|||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
|
|
|
@ -36,12 +36,9 @@
|
|||
"dependencies/python3-borgbackup.json",
|
||||
"dependencies/python3-llfuse.json",
|
||||
"dependencies/python3-appdirs.json",
|
||||
"dependencies/python3-apscheduler.json",
|
||||
"dependencies/python3-paramiko.json",
|
||||
"dependencies/python3-peewee.json",
|
||||
"dependencies/python3-psutil.json",
|
||||
"dependencies/python3-python-dateutil.json",
|
||||
"dependencies/python3-qdarkstyle.json",
|
||||
"dependencies/python3-secretstorage.json",
|
||||
"dependencies/python3-pytest-runner.json",
|
||||
{
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"name": "python3-apscheduler",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} apscheduler"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl",
|
||||
"sha256": "3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/6a/9a/50fadfd53ec909e4399b67c74cc7f4e883488035cfcdb90b685758fa8b34/setuptools-41.4.0-py2.py3-none-any.whl",
|
||||
"sha256": "8d01f7ee4191d9fdcd9cc5796f75199deccb25b154eba82d44d6a042cf873670"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/e7/f9/f0b53f88060247251bf481fa6ea62cd0d25bf1b11a87888e53ce5b7c8ad2/pytz-2019.3-py2.py3-none-any.whl",
|
||||
"sha256": "1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/ef/99/53bd1ac9349262f59c1c421d8fcc2559ae8a5eeffed9202684756b648d33/tzlocal-2.0.0-py2.py3-none-any.whl",
|
||||
"sha256": "11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/09/ff/d5b0e81846cd5e92d02e5f2682b78c73a5d9d61bc1eae32cea5ac15c0d47/APScheduler-3.6.1-py2.py3-none-any.whl",
|
||||
"sha256": "cde18f6dbffa1b75aff67fd7fe423a3020cb0363f6c67bd45f24306d90898231"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"name": "python3-dateutil",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} dateutil"
|
||||
],
|
||||
"sources": []
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "python3-python-dateutil",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} python-dateutil"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl",
|
||||
"sha256": "3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/41/17/c62faccbfbd163c7f57f3844689e3a78bae1f403648a6afb1d0866d87fbb/python_dateutil-2.8.0-py2.py3-none-any.whl",
|
||||
"sha256": "7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"name": "python3-qdarkstyle",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} qdarkstyle"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/45/dc/3241eef99eb45f1def35cf93af35d1cf9ef4c0991792583b8f33ea41b092/more_itertools-7.2.0-py3-none-any.whl",
|
||||
"sha256": "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/74/3d/1ee25a26411ba0401b43c6376d2316a71addcc72ef8690b101b4ea56d76a/zipp-0.6.0-py2.py3-none-any.whl",
|
||||
"sha256": "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/f6/d2/40b3fa882147719744e6aa50ac39cf7a22a913cbcba86a0371176c425a3b/importlib_metadata-0.23-py2.py3-none-any.whl",
|
||||
"sha256": "d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/1c/ca/5b8c1fe032a458c2c4bcbe509d1401dca9dda35c7fc46b36bb81c2834740/psutil-5.6.3.tar.gz",
|
||||
"sha256": "863a85c1c0a5103a12c05a35e59d336e1d665747e531256e061213e2e90f63f3"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/56/23/7bc32827ea3a34bdb19bded6c6be86e4ae69fceb21a5e27956fa6ef624f2/helpdev-0.6.10-py3-none-any.whl",
|
||||
"sha256": "6cb2c604d97101a47b81d6d234b48e0f452efe4490b4cc67c33708420c2deaa0"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/7b/30/b528832a64e09bba180d14219262ed35f611863d032fee468e5686b52e21/QDarkStyle-2.7-py2.py3-none-any.whl",
|
||||
"sha256": "c012bfaa482a1444b477b43a0b3585ea2f844ac16bfa2dff40ea535ce669c054"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -15,10 +15,10 @@ classifiers =
|
|||
Operating System :: MacOS
|
||||
Operating System :: POSIX
|
||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Topic :: System :: Archiving :: Backup
|
||||
Topic :: System :: Systems Administration
|
||||
Topic :: Utilities
|
||||
|
@ -31,7 +31,7 @@ project_urls =
|
|||
Source Code = https://github.com/borgbase/vorta
|
||||
|
||||
[options]
|
||||
python_requires = >= 3.6
|
||||
python_requires = >=3.7
|
||||
setup_requires =
|
||||
pip >= 10
|
||||
pytest-runner
|
||||
|
@ -41,12 +41,9 @@ install_requires =
|
|||
paramiko
|
||||
pyqt5
|
||||
peewee
|
||||
python-dateutil
|
||||
apscheduler < 4.0
|
||||
psutil
|
||||
setuptools
|
||||
secretstorage; sys_platform != 'darwin'
|
||||
tzlocal==2.1
|
||||
pyobjc-core; sys_platform == 'darwin'
|
||||
pyobjc-framework-Cocoa; sys_platform == 'darwin'
|
||||
pyobjc-framework-LaunchServices; sys_platform == 'darwin'
|
||||
|
|
|
@ -57,7 +57,7 @@ class VortaApp(QtSingleApplication):
|
|||
init_translations(self)
|
||||
|
||||
self.setQuitOnLastWindowClosed(False)
|
||||
self.scheduler = VortaScheduler(self)
|
||||
self.scheduler = VortaScheduler()
|
||||
|
||||
self.setApplicationName("Vorta")
|
||||
|
||||
|
@ -73,7 +73,6 @@ class VortaApp(QtSingleApplication):
|
|||
elif SettingsModel.get(key='foreground').value:
|
||||
self.open_main_window_action()
|
||||
|
||||
self.backup_cancelled_event.connect(self.scheduler.cancel_all_jobs)
|
||||
self.backup_started_event.connect(self.backup_started_event_response)
|
||||
self.backup_finished_event.connect(self.backup_finished_event_response)
|
||||
self.backup_cancelled_event.connect(self.backup_cancelled_event_response)
|
||||
|
@ -107,7 +106,6 @@ class VortaApp(QtSingleApplication):
|
|||
|
||||
def quit_app_action(self):
|
||||
self.backup_cancelled_event.emit()
|
||||
self.scheduler.shutdown()
|
||||
del self.main_window
|
||||
self.tray.deleteLater()
|
||||
del self.tray
|
||||
|
@ -147,6 +145,7 @@ class VortaApp(QtSingleApplication):
|
|||
self.tray.set_tray_icon()
|
||||
|
||||
def backup_cancelled_event_response(self):
|
||||
self.scheduler.cancel_all_jobs()
|
||||
self.tray.set_tray_icon()
|
||||
|
||||
def message_received_event_response(self, message):
|
||||
|
@ -178,6 +177,7 @@ class VortaApp(QtSingleApplication):
|
|||
borg_compat.set_version(result['data']['version'], result['data']['path'])
|
||||
self.main_window.miscTab.set_borg_details(borg_compat.version, borg_compat.path)
|
||||
self.main_window.repoTab.toggle_available_compression()
|
||||
self.scheduler.reload_all_timers() # Start timer after Borg version is set.
|
||||
else:
|
||||
self._alert_missing_borg()
|
||||
|
||||
|
|
|
@ -21,12 +21,11 @@
|
|||
<widget class="QToolBox" name="toolBox">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>3</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="schedule">
|
||||
<property name="geometry">
|
||||
|
@ -39,7 +38,6 @@
|
|||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
@ -54,284 +52,268 @@
|
|||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>7</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleOffRadio">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Backup manually</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleIntervalRadio">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Backup every </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="scheduleIntervalHours">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>740</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item alignment="Qt::AlignLeft">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>hours at</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="scheduleIntervalMinutes">
|
||||
<property name="maximum">
|
||||
<number>59</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>minutes past the hour</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleFixedRadio">
|
||||
<property name="text">
|
||||
<string>Backup daily at</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTimeEdit" name="scheduleFixedTime"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="validationCheckBox">
|
||||
<property name="text">
|
||||
<string>Validate repository data every</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="validationSpinBox">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>52</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>weeks</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="pruneCheckBox">
|
||||
<property name="text">
|
||||
<string>Prune old Archives after each backup</string>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="scheduleApplyButton">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
<item row="0" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<property name="bottomMargin">
|
||||
<number>7</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleOffRadio">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Backup manually</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
<item row="2" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleFixedRadio">
|
||||
<property name="text">
|
||||
<string>Backup daily at</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTimeEdit" name="scheduleFixedTime"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Next Backup:</string>
|
||||
</property>
|
||||
</widget>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="scheduleIntervalRadio">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Backup every </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="scheduleIntervalCount">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>740</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="scheduleIntervalUnit"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="nextBackupDateTimeLabel">
|
||||
<property name="text">
|
||||
<string>Off</string>
|
||||
<item row="3" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Next Backup:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="nextBackupDateTimeLabel">
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="pruneCheckBox">
|
||||
<property name="text">
|
||||
<string>Prune old Archives after each backup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</spacer>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="missedBackupsCheckBox">
|
||||
<property name="text">
|
||||
<string>Run missed backups right after startup or wakeup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="validationCheckBox">
|
||||
<property name="text">
|
||||
<string>Validate repository data every</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="validationWeeksCount">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>52</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>weeks</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
|
@ -355,8 +337,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>663</width>
|
||||
<height>354</height>
|
||||
<width>282</width>
|
||||
<height>146</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
|
@ -416,8 +398,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>663</width>
|
||||
<height>354</height>
|
||||
<width>98</width>
|
||||
<height>86</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
|
@ -432,7 +414,6 @@
|
|||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
@ -479,8 +460,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>663</width>
|
||||
<height>354</height>
|
||||
<width>494</width>
|
||||
<height>288</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
|
@ -531,7 +512,6 @@
|
|||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
|
|
@ -7,6 +7,7 @@ import shlex
|
|||
import select
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime as dt
|
||||
from collections import namedtuple
|
||||
from threading import Lock
|
||||
from PyQt5 import QtCore
|
||||
|
@ -24,7 +25,7 @@ temp_mutex = Lock()
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments', 'encryption'])
|
||||
FakeProfile = namedtuple('FakeProfile', ['repo', 'name', 'ssh_key'])
|
||||
FakeProfile = namedtuple('FakeProfile', ['id', 'repo', 'name', 'ssh_key'])
|
||||
|
||||
"""
|
||||
All methods in this class must be thread safe. Particularly,
|
||||
|
@ -180,6 +181,7 @@ class BorgJob(Job, BackupProfileMixin):
|
|||
ret['repo_url'] = profile.repo.url
|
||||
ret['extra_borg_arguments'] = profile.repo.extra_borg_arguments
|
||||
ret['profile_name'] = profile.name
|
||||
ret['profile_id'] = profile.id
|
||||
|
||||
ret['ok'] = True
|
||||
|
||||
|
@ -205,9 +207,9 @@ class BorgJob(Job, BackupProfileMixin):
|
|||
|
||||
def run(self):
|
||||
self.started_event()
|
||||
log_entry = EventLogModel(category='borg-run',
|
||||
log_entry = EventLogModel(category=self.params.get('category', 'user'),
|
||||
subcommand=self.cmd[1],
|
||||
profile=self.params.get('profile_name', None)
|
||||
profile=self.params.get('profile_id', None)
|
||||
)
|
||||
log_entry.save()
|
||||
logger.info('Running command %s', ' '.join(self.cmd))
|
||||
|
@ -285,6 +287,7 @@ class BorgJob(Job, BackupProfileMixin):
|
|||
|
||||
log_entry.returncode = p.returncode
|
||||
log_entry.repo_url = self.params.get('repo_url', None)
|
||||
log_entry.end_time = dt.now()
|
||||
log_entry.save()
|
||||
|
||||
self.process_result(result)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import tempfile
|
||||
from dateutil import parser
|
||||
import subprocess
|
||||
from datetime import datetime as dt
|
||||
|
||||
from vorta.i18n import trans_late
|
||||
from vorta.utils import format_archive_name, borg_compat, get_network_status_monitor
|
||||
|
@ -16,7 +16,7 @@ class BorgCreateJob(BorgJob):
|
|||
snapshot_id=result['data']['archive']['id'],
|
||||
defaults={
|
||||
'name': result['data']['archive']['name'],
|
||||
'time': parser.parse(result['data']['archive']['start']),
|
||||
'time': dt.fromisoformat(result['data']['archive']['start']),
|
||||
'repo': result['params']['repo_id'],
|
||||
'duration': result['data']['archive']['duration'],
|
||||
'size': result['data']['archive']['stats']['deduplicated_size']
|
||||
|
|
|
@ -16,6 +16,7 @@ class BorgInfoRepoJob(BorgJob):
|
|||
|
||||
# Build fake profile because we don't have it in the DB yet. Assume unencrypted.
|
||||
profile = FakeProfile(
|
||||
999,
|
||||
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'], 'none'),
|
||||
'New Repo',
|
||||
params['ssh_key']
|
||||
|
|
|
@ -12,6 +12,7 @@ class BorgInitJob(BorgJob):
|
|||
|
||||
# Build fake profile because we don't have it in the DB yet.
|
||||
profile = FakeProfile(
|
||||
999,
|
||||
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'],
|
||||
params['encryption']), 'Init Repo', params['ssh_key']
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from dateutil import parser
|
||||
from datetime import datetime as dt
|
||||
from .borg_job import BorgJob
|
||||
from vorta.models import ArchiveModel, RepoModel
|
||||
|
||||
|
@ -49,7 +49,7 @@ class BorgListRepoJob(BorgJob):
|
|||
repo=repo.id,
|
||||
defaults={
|
||||
'name': archive['name'],
|
||||
'time': parser.parse(archive['time'])
|
||||
'time': dt.fromisoformat(archive['time'])
|
||||
}
|
||||
)
|
||||
new_archive.save()
|
||||
|
|
|
@ -8,7 +8,6 @@ import json
|
|||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta as rd
|
||||
|
||||
import peewee as pw
|
||||
from playhouse.migrate import SqliteMigrator, migrate
|
||||
|
@ -16,7 +15,7 @@ from playhouse.migrate import SqliteMigrator, migrate
|
|||
from vorta.i18n import trans_late
|
||||
from vorta.utils import slugify
|
||||
|
||||
SCHEMA_VERSION = 17
|
||||
SCHEMA_VERSION = 18
|
||||
|
||||
db = pw.Proxy()
|
||||
|
||||
|
@ -75,10 +74,13 @@ class BackupProfileModel(pw.Model):
|
|||
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_interval_count = pw.IntegerField(default=3)
|
||||
schedule_interval_unit = pw.CharField(default='hours')
|
||||
schedule_fixed_hour = pw.IntegerField(default=3)
|
||||
schedule_fixed_minute = pw.IntegerField(default=42)
|
||||
schedule_interval_hours = pw.IntegerField(default=3) # no longer used
|
||||
schedule_interval_minutes = pw.IntegerField(default=42) # no longer used
|
||||
schedule_make_up_missed = pw.BooleanField(default=True)
|
||||
validation_on = pw.BooleanField(default=True)
|
||||
validation_weeks = pw.IntegerField(default=3)
|
||||
prune_on = pw.BooleanField(default=False)
|
||||
|
@ -148,6 +150,7 @@ class WifiSettingModel(pw.Model):
|
|||
class EventLogModel(pw.Model):
|
||||
"""Keep a log of background jobs."""
|
||||
start_time = pw.DateTimeField(default=datetime.now)
|
||||
end_time = pw.DateTimeField(default=datetime.now)
|
||||
category = pw.CharField()
|
||||
subcommand = pw.CharField(null=True)
|
||||
message = pw.CharField(null=True)
|
||||
|
@ -414,6 +417,19 @@ def init_db(con=None):
|
|||
'create_backup_cmd', pw.CharField(default=''))
|
||||
)
|
||||
|
||||
if current_schema.version < 18:
|
||||
_apply_schema_update(
|
||||
current_schema, 18,
|
||||
migrator.add_column(BackupProfileModel._meta.table_name,
|
||||
'schedule_interval_unit', pw.CharField(default='hours')),
|
||||
migrator.add_column(BackupProfileModel._meta.table_name,
|
||||
'schedule_interval_count', pw.IntegerField(default=3)),
|
||||
migrator.add_column(BackupProfileModel._meta.table_name,
|
||||
'schedule_make_up_missed', pw.BooleanField(default=False)),
|
||||
migrator.add_column(EventLogModel._meta.table_name,
|
||||
'end_time', pw.DateTimeField(default=datetime.now))
|
||||
)
|
||||
|
||||
# Create missing settings and update labels. Leave setting values untouched.
|
||||
for setting in get_misc_settings():
|
||||
s, created = SettingsModel.get_or_create(key=setting['key'], defaults=setting)
|
||||
|
@ -421,5 +437,5 @@ def init_db(con=None):
|
|||
s.save()
|
||||
|
||||
# Delete old log entries after 3 months.
|
||||
three_months_ago = datetime.now() - rd(months=3)
|
||||
three_months_ago = datetime.now() - timedelta(days=3)
|
||||
EventLogModel.delete().where(EventLogModel.start_time < three_months_ago).execute()
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import logging
|
||||
from datetime import date, timedelta
|
||||
from datetime import datetime as dt, date, timedelta
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from apscheduler.schedulers.qt import QtScheduler
|
||||
from apscheduler.triggers import cron
|
||||
from vorta.borg.check import BorgCheckJob
|
||||
from vorta.borg.create import BorgCreateJob
|
||||
from vorta.borg.job_scheduler import DEBUG, JobsManager
|
||||
|
@ -17,101 +15,136 @@ from vorta.notifications import VortaNotifications
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: refactor to use QtCore.QTimer directly
|
||||
class VortaScheduler(QtScheduler):
|
||||
def __init__(self, parent):
|
||||
class VortaScheduler(QtCore.QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.app = parent
|
||||
self.start()
|
||||
self.jobs_manager = JobsManager()
|
||||
self.reload()
|
||||
self.jobs_manager = JobsManager() # push scheduled jobs to JobManager for execution
|
||||
self.timers = dict() # keep mapping of profiles to timers
|
||||
|
||||
# Set timer to make sure background tasks are scheduled
|
||||
# Set additional timer to make sure background tasks stay scheduled.
|
||||
# E.g. after hibernation
|
||||
self.qt_timer = QtCore.QTimer()
|
||||
self.qt_timer.timeout.connect(self.reload)
|
||||
self.qt_timer.setInterval(45 * 60 * 1000)
|
||||
self.qt_timer.timeout.connect(self.reload_all_timers)
|
||||
self.qt_timer.setInterval(15 * 60 * 1000)
|
||||
self.qt_timer.start()
|
||||
|
||||
def cancel_all_jobs(self):
|
||||
if DEBUG:
|
||||
print("Cancel all Jobs on Vorta Queue")
|
||||
logger.debug('Cancelling all jobs on job queue')
|
||||
self.jobs_manager.cancel_all_jobs()
|
||||
|
||||
def tr(self, *args, **kwargs):
|
||||
scope = self.__class__.__name__
|
||||
return translate(scope, *args, **kwargs)
|
||||
|
||||
def reload(self):
|
||||
def set_timer_for_profile(self, profile_id):
|
||||
"""
|
||||
Set a timer for next scheduled backup run of this profile.
|
||||
|
||||
Does nothing if set to manual backups or no repo is assigned.
|
||||
|
||||
Else will look for previous scheduled backups and catch up if
|
||||
schedule_make_up_missed is enabled.
|
||||
|
||||
Or, if catch-up is not enabled, will add interval to last run to find
|
||||
next suitable backup time.
|
||||
"""
|
||||
|
||||
# Stop and remove any existing timer for this profile
|
||||
if profile_id in self.timers:
|
||||
self.timers[profile_id]['qtt'].stop()
|
||||
del self.timers[profile_id]
|
||||
|
||||
profile = BackupProfileModel.get_or_none(id=profile_id)
|
||||
if profile is None \
|
||||
or profile.repo is None \
|
||||
or profile.schedule_mode == 'off':
|
||||
return
|
||||
|
||||
logger.info('Setting timer for profile %s', profile_id)
|
||||
|
||||
last_run_log = EventLogModel.select().where(
|
||||
EventLogModel.subcommand == 'create',
|
||||
EventLogModel.category == 'scheduled',
|
||||
EventLogModel.profile == profile.id,
|
||||
).order_by(EventLogModel.end_time.desc()).first()
|
||||
|
||||
# Desired interval between scheduled backups. Uses datetime.timedelta() units.
|
||||
if profile.schedule_mode == 'interval':
|
||||
interval = {profile.schedule_interval_unit: profile.schedule_interval_count}
|
||||
elif profile.schedule_mode == 'fixed':
|
||||
interval = {'days': 1}
|
||||
|
||||
# If last run was too long ago and catch-up is enabled, run now
|
||||
if profile.schedule_make_up_missed \
|
||||
and last_run_log is not None \
|
||||
and last_run_log.end_time + timedelta(**interval) < dt.now():
|
||||
logger.debug('Catching up by running job for %s', profile.name)
|
||||
self.create_backup(profile.id)
|
||||
return
|
||||
|
||||
# If the job never ran, use midnight as random starting point
|
||||
if last_run_log is None:
|
||||
last_run = dt.now().replace(hour=0, minute=0)
|
||||
else:
|
||||
last_run = last_run_log.end_time
|
||||
|
||||
# Squash seconds to get nice starting time
|
||||
last_run = last_run.replace(second=0, microsecond=0)
|
||||
|
||||
# Fixed time is a special case of days=1 interval
|
||||
if profile.schedule_mode == 'fixed':
|
||||
last_run = last_run.replace(hour=profile.schedule_fixed_hour, minute=profile.schedule_fixed_minute)
|
||||
|
||||
# Add interval to last run time to arrive at next run.
|
||||
next_run = last_run
|
||||
now = dt.now()
|
||||
while next_run < now:
|
||||
next_run += timedelta(**interval)
|
||||
|
||||
logger.debug('Scheduling next run for %s', next_run)
|
||||
timer_ms = (next_run - dt.now()).total_seconds() * 1000
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(int(timer_ms))
|
||||
timer.timeout.connect(lambda: self.create_backup(profile_id))
|
||||
timer.start()
|
||||
self.timers[profile_id] = {'qtt': timer, 'dt': next_run}
|
||||
|
||||
def reload_all_timers(self):
|
||||
logger.debug('Refreshing all scheduler timers')
|
||||
for profile in BackupProfileModel.select():
|
||||
trigger = None
|
||||
job_id = f'{profile.id}'
|
||||
if profile.schedule_mode == 'interval':
|
||||
if profile.schedule_interval_hours >= 24:
|
||||
days = profile.schedule_interval_hours // 24
|
||||
leftover_hours = profile.schedule_interval_hours % 24
|
||||
|
||||
if leftover_hours == 0:
|
||||
cron_hours = '1'
|
||||
else:
|
||||
cron_hours = f'*/{leftover_hours}'
|
||||
|
||||
trigger = cron.CronTrigger(day=f'*/{days}',
|
||||
hour=cron_hours,
|
||||
minute=profile.schedule_interval_minutes)
|
||||
else:
|
||||
trigger = cron.CronTrigger(hour=f'*/{profile.schedule_interval_hours}',
|
||||
minute=profile.schedule_interval_minutes)
|
||||
elif profile.schedule_mode == 'fixed':
|
||||
trigger = cron.CronTrigger(hour=profile.schedule_fixed_hour,
|
||||
minute=profile.schedule_fixed_minute)
|
||||
if self.get_job(job_id) is not None and trigger is not None:
|
||||
self.reschedule_job(job_id, trigger=trigger)
|
||||
logger.debug('Job for profile %s was rescheduled.', profile.name)
|
||||
elif trigger is not None:
|
||||
if profile.repo is not None:
|
||||
repo_id = profile.repo.id
|
||||
else:
|
||||
repo_id = -1
|
||||
self.add_job(
|
||||
func=self.create_backup,
|
||||
args=[profile.id, repo_id],
|
||||
trigger=trigger,
|
||||
id=job_id,
|
||||
misfire_grace_time=180
|
||||
)
|
||||
logger.debug('New job for profile %s was added.', profile.name)
|
||||
elif self.get_job(job_id) is not None and trigger is None:
|
||||
self.remove_job(job_id)
|
||||
logger.debug('Job for profile %s was removed.', profile.name)
|
||||
self.set_timer_for_profile(profile.id)
|
||||
|
||||
@property
|
||||
def next_job(self):
|
||||
self.wakeup()
|
||||
self._process_jobs()
|
||||
jobs = []
|
||||
for job in self.get_jobs():
|
||||
jobs.append((job.next_run_time, job.id))
|
||||
next_job = dt.now()
|
||||
next_profile = None
|
||||
for profile_id, timer in self.timers.items():
|
||||
if timer['dt'] > next_job and timer['qtt'].isActive():
|
||||
next_job = timer['dt']
|
||||
next_profile = profile_id
|
||||
|
||||
if jobs:
|
||||
jobs.sort(key=lambda job: job[0])
|
||||
profile = BackupProfileModel.get(id=int(jobs[0][1]))
|
||||
return f"{jobs[0][0].strftime('%H:%M')} ({profile.name})"
|
||||
if next_profile is not None:
|
||||
profile = BackupProfileModel.get_or_none(id=next_profile)
|
||||
return f"{next_job.strftime('%H:%M')} ({profile.name})"
|
||||
else:
|
||||
return self.tr('None scheduled')
|
||||
|
||||
def next_job_for_profile(self, profile_id):
|
||||
self.wakeup()
|
||||
job = self.get_job(str(profile_id))
|
||||
job = self.timers.get(profile_id)
|
||||
if job is None:
|
||||
return self.tr('None scheduled')
|
||||
else:
|
||||
return job.next_run_time.strftime('%Y-%m-%d %H:%M')
|
||||
return job['dt'].strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
def create_backup(self, profile_id, repo_id):
|
||||
if DEBUG:
|
||||
print("start backup for profile ", profile_id)
|
||||
def create_backup(self, profile_id):
|
||||
logger.debug('Start scheduled backup for profile %s', profile_id)
|
||||
notifier = VortaNotifications.pick()
|
||||
profile = BackupProfileModel.get(id=profile_id)
|
||||
profile = BackupProfileModel.get_or_none(id=profile_id)
|
||||
|
||||
if profile is None:
|
||||
logger.info('Profile not found. Maybe deleted?')
|
||||
return
|
||||
|
||||
logger.info('Starting background backup for %s', profile.name)
|
||||
notifier.deliver(self.tr('Vorta Backup'),
|
||||
|
@ -120,7 +153,8 @@ class VortaScheduler(QtScheduler):
|
|||
msg = BorgCreateJob.prepare(profile)
|
||||
if msg['ok']:
|
||||
logger.info('Preparation for backup successful.')
|
||||
job = BorgCreateJob(msg['cmd'], msg, repo_id)
|
||||
msg['category'] = 'scheduled'
|
||||
job = BorgCreateJob(msg['cmd'], msg, profile.repo.id)
|
||||
job.result.connect(self.notify)
|
||||
self.jobs_manager.add_job(job)
|
||||
|
||||
|
@ -146,6 +180,8 @@ class VortaScheduler(QtScheduler):
|
|||
notifier.deliver(self.tr('Vorta Backup'), self.tr('Error during backup creation.'), level='error')
|
||||
logger.error('Error during backup creation.')
|
||||
|
||||
self.set_timer_for_profile(profile_id)
|
||||
|
||||
def post_backup_tasks(self, profile_id):
|
||||
"""
|
||||
Pruning and checking after successful backup.
|
||||
|
|
|
@ -59,7 +59,6 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
|||
self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile)
|
||||
self.repoTab.repo_changed.connect(self.scheduleTab.populate_from_profile)
|
||||
self.repoTab.repo_added.connect(self.archiveTab.list_action)
|
||||
self.tabWidget.currentChanged.connect(self.scheduleTab._draw_next_scheduled_backup)
|
||||
|
||||
self.createStartBtn.clicked.connect(self.app.create_backup_action)
|
||||
self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit)
|
||||
|
|
|
@ -39,13 +39,27 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
self.logTableWidget.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.logTableWidget.setEditTriggers(QTableView.NoEditTriggers)
|
||||
|
||||
# Scheduler intervals we know
|
||||
self.scheduleIntervalUnit.addItem(self.tr('Minutes'), 'minutes')
|
||||
self.scheduleIntervalUnit.addItem(self.tr('Hours'), 'hours')
|
||||
self.scheduleIntervalUnit.addItem(self.tr('Days'), 'days')
|
||||
self.scheduleIntervalUnit.addItem(self.tr('Weeks'), 'weeks')
|
||||
|
||||
# Populate with data
|
||||
self.populate_from_profile()
|
||||
self.set_icons()
|
||||
|
||||
# Connect events
|
||||
self.scheduleApplyButton.clicked.connect(self.on_scheduler_apply)
|
||||
self.app.backup_finished_event.connect(self.populate_logs)
|
||||
|
||||
# Scheduler events
|
||||
for label, obj in self.schedulerRadioMapping.items():
|
||||
obj.clicked.connect(self.on_scheduler_change)
|
||||
self.scheduleIntervalCount.valueChanged.connect(self.on_scheduler_change)
|
||||
self.scheduleIntervalUnit.currentIndexChanged.connect(self.on_scheduler_change)
|
||||
self.scheduleFixedTime.timeChanged.connect(self.on_scheduler_change)
|
||||
|
||||
# Network and shell commands events
|
||||
self.dontRunOnMeteredNetworksCheckBox.stateChanged.connect(
|
||||
lambda new_val, attr='dont_run_on_metered_networks': self.save_profile_attr(attr, new_val))
|
||||
self.postBackupCmdLineEdit.textEdited.connect(
|
||||
|
@ -54,6 +68,29 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
lambda new_val, attr='pre_backup_cmd': self.save_profile_attr(attr, new_val))
|
||||
self.createCmdLineEdit.textEdited.connect(
|
||||
lambda new_val, attr='create_backup_cmd': self.save_repo_attr(attr, new_val))
|
||||
self.missedBackupsCheckBox.stateChanged.connect(
|
||||
lambda new_val, attr='schedule_make_up_missed': self.save_profile_attr(attr, new_val))
|
||||
self.pruneCheckBox.stateChanged.connect(
|
||||
lambda new_val, attr='prune_on': self.save_profile_attr(attr, new_val))
|
||||
self.validationCheckBox.stateChanged.connect(
|
||||
lambda new_val, attr='validation_on': self.save_profile_attr(attr, new_val))
|
||||
self.validationWeeksCount.valueChanged.connect(
|
||||
lambda new_val, attr='validation_weeks': self.save_profile_attr(attr, new_val))
|
||||
|
||||
def on_scheduler_change(self, _):
|
||||
profile = self.profile()
|
||||
# Save scheduler settings, apply new scheduler and display next task for profile.
|
||||
for label, obj in self.schedulerRadioMapping.items():
|
||||
if obj.isChecked():
|
||||
profile.schedule_mode = label
|
||||
profile.schedule_interval_unit = self.scheduleIntervalUnit.currentData()
|
||||
profile.schedule_interval_count = self.scheduleIntervalCount.value()
|
||||
qtime = self.scheduleFixedTime.time()
|
||||
profile.schedule_fixed_hour, profile.schedule_fixed_minute = qtime.hour(), qtime.minute()
|
||||
profile.save()
|
||||
|
||||
self.app.scheduler.set_timer_for_profile(profile.id)
|
||||
self.draw_next_scheduled_backup()
|
||||
|
||||
def set_icons(self):
|
||||
self.toolBox.setItemIcon(0, get_colored_icon('clock-o'))
|
||||
|
@ -66,20 +103,26 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
profile = self.profile()
|
||||
self.schedulerRadioMapping[profile.schedule_mode].setChecked(True)
|
||||
|
||||
self.scheduleIntervalHours.setValue(profile.schedule_interval_hours)
|
||||
self.scheduleIntervalMinutes.setValue(profile.schedule_interval_minutes)
|
||||
# Set interval scheduler options
|
||||
self.scheduleIntervalUnit.setCurrentIndex(
|
||||
self.scheduleIntervalUnit.findData(profile.schedule_interval_unit))
|
||||
self.scheduleIntervalCount.setValue(profile.schedule_interval_count)
|
||||
|
||||
# Set fixed daily time scheduler options
|
||||
self.scheduleFixedTime.setTime(
|
||||
QtCore.QTime(profile.schedule_fixed_hour, profile.schedule_fixed_minute))
|
||||
|
||||
# Set checking options
|
||||
self.validationCheckBox.setCheckState(profile.validation_on)
|
||||
self.validationSpinBox.setValue(profile.validation_weeks)
|
||||
# Set borg-check options
|
||||
self.validationCheckBox.setCheckState(QtCore.Qt.Checked if profile.validation_on else QtCore.Qt.Unchecked)
|
||||
self.validationWeeksCount.setValue(profile.validation_weeks)
|
||||
|
||||
self.pruneCheckBox.setCheckState(profile.prune_on)
|
||||
self.validationCheckBox.setTristate(False)
|
||||
self.pruneCheckBox.setTristate(False)
|
||||
|
||||
self.dontRunOnMeteredNetworksCheckBox.setChecked(profile.dont_run_on_metered_networks)
|
||||
# Other checkbox options
|
||||
self.pruneCheckBox.setCheckState(
|
||||
QtCore.Qt.Checked if profile.prune_on else QtCore.Qt.Unchecked)
|
||||
self.missedBackupsCheckBox.setCheckState(
|
||||
QtCore.Qt.Checked if profile.schedule_make_up_missed else QtCore.Qt.Unchecked)
|
||||
self.dontRunOnMeteredNetworksCheckBox.setChecked(
|
||||
QtCore.Qt.Checked if profile.dont_run_on_metered_networks else QtCore.Qt.Unchecked)
|
||||
|
||||
self.preBackupCmdLineEdit.setText(profile.pre_backup_cmd)
|
||||
self.postBackupCmdLineEdit.setText(profile.post_backup_cmd)
|
||||
|
@ -89,10 +132,13 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
else:
|
||||
self.createCmdLineEdit.setEnabled(False)
|
||||
|
||||
self._draw_next_scheduled_backup()
|
||||
self.populate_wifi()
|
||||
self.populate_logs()
|
||||
|
||||
def draw_next_scheduled_backup(self):
|
||||
self.nextBackupDateTimeLabel.setText(self.app.scheduler.next_job_for_profile(self.profile().id))
|
||||
self.nextBackupDateTimeLabel.repaint()
|
||||
|
||||
def populate_wifi(self):
|
||||
self.wifiListWidget.clear()
|
||||
for wifi in get_sorted_wifis(self.profile()):
|
||||
|
@ -135,27 +181,3 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
self.logTableWidget.setItem(row, LogTableColumn.Repository, QTableWidgetItem(log_line.repo_url))
|
||||
self.logTableWidget.setItem(row, LogTableColumn.ReturnCode, QTableWidgetItem(str(log_line.returncode)))
|
||||
self.logTableWidget.setSortingEnabled(sorting) # restore sorting now that modifications are done
|
||||
|
||||
def _draw_next_scheduled_backup(self):
|
||||
self.nextBackupDateTimeLabel.setText(self.app.scheduler.next_job_for_profile(self.profile().id))
|
||||
self.nextBackupDateTimeLabel.repaint()
|
||||
|
||||
def on_scheduler_apply(self):
|
||||
profile = self.profile()
|
||||
|
||||
# Save checking options
|
||||
profile.validation_weeks = self.validationSpinBox.value()
|
||||
profile.validation_on = self.validationCheckBox.isChecked()
|
||||
profile.prune_on = self.pruneCheckBox.isChecked()
|
||||
|
||||
# Save scheduler timing and activate if needed.
|
||||
for label, obj in self.schedulerRadioMapping.items():
|
||||
if obj.isChecked():
|
||||
profile.schedule_mode = label
|
||||
profile.schedule_interval_hours = self.scheduleIntervalHours.value()
|
||||
profile.schedule_interval_minutes = self.scheduleIntervalMinutes.value()
|
||||
qtime = self.scheduleFixedTime.time()
|
||||
profile.schedule_fixed_hour, profile.schedule_fixed_minute = qtime.hour(), qtime.minute()
|
||||
profile.save()
|
||||
self.app.scheduler.reload()
|
||||
self._draw_next_scheduled_backup()
|
||||
|
|
|
@ -72,7 +72,7 @@ def init_db(qapp, qtbot, tmpdir_factory):
|
|||
qapp.main_window = MainWindow(qapp) # Re-open main window to apply mock data in UI
|
||||
|
||||
yield
|
||||
qapp.backup_cancelled_event.emit()
|
||||
# qapp.backup_cancelled_event.emit()
|
||||
qtbot.waitUntil(lambda: not vorta.borg.job_scheduler.JobsManager.is_worker_running())
|
||||
mock_db.close()
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
"exclude_patterns": null,
|
||||
"exclude_if_present": ".nobackup",
|
||||
"schedule_mode": "off",
|
||||
"schedule_interval_hours": 1,
|
||||
"schedule_interval_minutes": 26,
|
||||
"schedule_interval_unit": "hours",
|
||||
"schedule_interval_count": 2,
|
||||
"schedule_fixed_hour": 16,
|
||||
"schedule_fixed_minute": 0,
|
||||
"validation_on": true,
|
||||
|
|
|
@ -19,7 +19,7 @@ def test_autostart(qapp, qtbot):
|
|||
if checkbox.text().startswith("Automatically"):
|
||||
# Have to use pos to click checkbox correctly
|
||||
# https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484
|
||||
qtbot.mouseClick(checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, checkbox.height() / 2))
|
||||
qtbot.mouseClick(checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(checkbox.height() / 2)))
|
||||
break
|
||||
|
||||
click_autostart()
|
||||
|
|
|
@ -5,21 +5,14 @@ from PyQt5 import QtCore
|
|||
def test_schedule_tab(qapp, qtbot):
|
||||
main = qapp.main_window
|
||||
tab = main.scheduleTab
|
||||
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
|
||||
qtbot.mouseClick(tab.scheduleOffRadio, QtCore.Qt.LeftButton)
|
||||
assert tab.nextBackupDateTimeLabel.text() == 'None scheduled'
|
||||
|
||||
tab.scheduleIntervalRadio.setChecked(True)
|
||||
tab.scheduleIntervalHours.setValue(5)
|
||||
tab.scheduleIntervalMinutes.setValue(10)
|
||||
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
|
||||
assert tab.nextBackupDateTimeLabel.text().startswith('20')
|
||||
tab.scheduleIntervalCount.setValue(5)
|
||||
qtbot.mouseClick(tab.scheduleIntervalRadio, QtCore.Qt.LeftButton)
|
||||
assert "None" not in tab.nextBackupDateTimeLabel.text()
|
||||
|
||||
tab.scheduleOffRadio.setChecked(True)
|
||||
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
|
||||
assert tab.nextBackupDateTimeLabel.text() == 'None scheduled'
|
||||
|
||||
tab.scheduleFixedRadio.setChecked(True)
|
||||
tab.scheduleFixedTime.setTime(QtCore.QTime(23, 59))
|
||||
qtbot.mouseClick(tab.scheduleApplyButton, QtCore.Qt.LeftButton)
|
||||
qtbot.mouseClick(tab.scheduleFixedRadio, QtCore.Qt.LeftButton)
|
||||
next_backup = dt.combine(date.today(), time(23, 59))
|
||||
assert tab.nextBackupDateTimeLabel.text() == next_backup.strftime('%Y-%m-%d %H:%M')
|
||||
|
|
|
@ -4,9 +4,10 @@ import vorta.models
|
|||
|
||||
|
||||
def test_scheduler_create_backup(qapp, qtbot, mocker, borg_json_output):
|
||||
events_before = vorta.models.EventLogModel.select().count()
|
||||
stdout, stderr = borg_json_output('create')
|
||||
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
|
||||
mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result)
|
||||
qapp.scheduler.create_backup(1, 1)
|
||||
qapp.scheduler.create_backup(1)
|
||||
|
||||
qtbot.waitUntil(lambda: vorta.models.EventLogModel.select().count() == 2, **pytest._wait_defaults)
|
||||
qtbot.waitUntil(lambda: vorta.models.EventLogModel.select().count() == events_before + 1, **pytest._wait_defaults)
|
||||
|
|
Loading…
Reference in New Issue