mirror of https://github.com/borgbase/vorta
Compare commits
20 Commits
v0.9.1-bet
...
master
Author | SHA1 | Date |
---|---|---|
shivansh02 | f252a8ec6a | |
Parnassius | 6b5f7a7aac | |
Manu | 9cabbbd193 | |
Aryaman Sharma | 3268bf1599 | |
Manu | 7642002573 | |
Sam | 58137f004d | |
Parnassius | 9b8dbcecfb | |
Adwait | bde55188e4 | |
Shivansh Singh | d721011c90 | |
Shivansh Singh | b2cf5b1fc9 | |
Shivansh Singh | 472c7c8996 | |
Hofer-Julian | d8cce255eb | |
Jeff Ramnani | 634f984e78 | |
Hofer-Julian | 0cc15e3d3d | |
Manu | 4665972076 | |
Manu | 9cc7a98838 | |
Manu | 1d85cb48dc | |
Manu | be6e08552a | |
TW | 675010e401 | |
Manu | 1f062359d8 |
|
@ -1,5 +1,5 @@
|
|||
name: "Bug Report Form"
|
||||
description: "Report a bug or a similiar issue."
|
||||
description: "Report a bug or a similar issue."
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Report a bug or a similiar issue - the classic way
|
||||
about: Report a bug or a similar issue - the classic way
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
@ -18,7 +18,7 @@ If you want to suggest a feature or have any other question, please use our
|
|||
#### Description
|
||||
|
||||
<!-- Description
|
||||
Please decribe your issue and its context in a clear and concise way.
|
||||
Please describe your issue and its context in a clear and concise way.
|
||||
Please try to reproduce the issue and provide the steps to reproduce it.
|
||||
-->
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
- [ ] All new and existing tests passed.
|
||||
|
||||
|
||||
*I provide my contribution under the terms of the [license](./../../LICENSE.txt) of this repository and I affirm the [Developer Certificate of Origin][dco].*
|
||||
*I provide my contribution under the terms of the [license](./../LICENSE.txt) of this repository and I affirm the [Developer Certificate of Origin][dco].*
|
||||
|
||||
[dco]: https://developercertificate.org/
|
||||
|
||||
|
|
|
@ -3,17 +3,21 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to use for building macOS release'
|
||||
description: 'Branch to use for building release'
|
||||
required: true
|
||||
default: 'master'
|
||||
borg_version:
|
||||
description: 'Borg version to package'
|
||||
required: true
|
||||
default: '1.2.1'
|
||||
default: '1.2.8'
|
||||
macos_version:
|
||||
description: 'macOS version for building'
|
||||
required: true
|
||||
default: 'macos-11'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-11
|
||||
runs-on: ${{ github.event.inputs.macos_version }}
|
||||
|
||||
steps:
|
||||
- name: Check out selected branch
|
||||
|
|
|
@ -6,10 +6,13 @@ on:
|
|||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-stale: 90
|
||||
days-before-pr-stale: -1
|
||||
days-before-issue-close: 7
|
||||
# days-before-pr-close: 10
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
Vorta is a backup client for macOS and Linux desktops. It integrates the mighty [BorgBackup](https://borgbackup.readthedocs.io) with your desktop environment to protect your data from disk failure, ransomware and theft.
|
||||
|
||||
![](https://files.qmax.us/vorta/screencast-8-small.gif)
|
||||
https://github.com/m3nu/vorta/assets/3916435/a622a148-5373-4ae0-87bc-4ca1d6f6202e
|
||||
|
||||
## Why is this great? 🤩
|
||||
|
||||
|
|
|
@ -28,10 +28,10 @@ else:
|
|||
@nox.parametrize("borgbackup", supported_borgbackup_versions)
|
||||
def run_tests(session, borgbackup):
|
||||
# install borgbackup
|
||||
if (sys.platform == 'darwin'):
|
||||
if sys.platform == 'darwin':
|
||||
# in macOS there's currently no fuse package which works with borgbackup directly
|
||||
session.install(f"borgbackup=={borgbackup}")
|
||||
elif (borgbackup == "1.1.18"):
|
||||
elif borgbackup == "1.1.18":
|
||||
# borgbackup 1.1.18 doesn't support pyfuse3
|
||||
session.install("llfuse")
|
||||
session.install(f"borgbackup[llfuse]=={borgbackup}")
|
||||
|
|
|
@ -18,10 +18,10 @@ def create_symlink(folder: Path) -> None:
|
|||
"""Create the appropriate symlink in the MacOS folder
|
||||
pointing to the Resources folder.
|
||||
"""
|
||||
sibbling = Path(str(folder).replace("MacOS", ""))
|
||||
sibling = Path(str(folder).replace("MacOS", ""))
|
||||
|
||||
# PyQt6/Qt/qml/QtQml/Models.2
|
||||
root = str(sibbling).partition("Contents")[2].lstrip("/")
|
||||
root = str(sibling).partition("Contents")[2].lstrip("/")
|
||||
# ../../../../
|
||||
backward = "../" * (root.count("/") + 1)
|
||||
# ../../../../Resources/PyQt6/Qt/qml/QtQml/Models.2
|
||||
|
@ -41,7 +41,7 @@ def fix_dll(dll: Path) -> None:
|
|||
|
||||
def match_func(pth: str) -> Optional[str]:
|
||||
"""Callback function for MachO.rewriteLoadCommands() that is
|
||||
called on every lookup path setted in the DLL headers.
|
||||
called on every lookup path set in the DLL headers.
|
||||
By returning None for system libraries, it changes nothing.
|
||||
Else we return a relative path pointing to the good file
|
||||
in the MacOS folder.
|
||||
|
@ -73,7 +73,7 @@ def find_problematic_folders(folder: Path) -> Generator[Path, None, None]:
|
|||
"""Recursively yields problematic folders (containing a dot in their name)."""
|
||||
for path in folder.iterdir():
|
||||
if not path.is_dir() or path.is_symlink():
|
||||
# Skip simlinks as they are allowed (even with a dot)
|
||||
# Skip symlinks as they are allowed (even with a dot)
|
||||
continue
|
||||
if "." in path.name:
|
||||
yield path
|
||||
|
@ -83,7 +83,7 @@ def find_problematic_folders(folder: Path) -> Generator[Path, None, None]:
|
|||
|
||||
def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]:
|
||||
"""Recursively move any non symlink file from a problematic folder
|
||||
to the sibbling one in Resources.
|
||||
to the sibling one in Resources.
|
||||
"""
|
||||
for path in folder.iterdir():
|
||||
if path.is_symlink():
|
||||
|
@ -91,10 +91,10 @@ def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]:
|
|||
if path.name == "qml":
|
||||
yield from move_contents_to_resources(path)
|
||||
else:
|
||||
sibbling = Path(str(path).replace("MacOS", "Resources"))
|
||||
sibbling.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(path, sibbling)
|
||||
yield sibbling
|
||||
sibling = Path(str(path).replace("MacOS", "Resources"))
|
||||
sibling.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(path, sibling)
|
||||
yield sibling
|
||||
|
||||
|
||||
def main(args: List[str]) -> int:
|
||||
|
|
|
@ -23,6 +23,7 @@ a = Analysis([os.path.join(SRC_DIR, '__main__.py')],
|
|||
datas=[
|
||||
(os.path.join(SRC_DIR, 'assets/UI/*'), 'assets/UI'),
|
||||
(os.path.join(SRC_DIR, 'assets/icons/*'), 'assets/icons'),
|
||||
(os.path.join(SRC_DIR, 'assets/exclusion_presets/*'), 'assets/exclusion_presets'),
|
||||
(os.path.join(SRC_DIR, 'i18n/qm/*'), 'vorta/i18n/qm'),
|
||||
],
|
||||
hiddenimports=[
|
||||
|
|
|
@ -47,6 +47,7 @@ install_requires =
|
|||
pyobjc-core < 10; sys_platform == 'darwin'
|
||||
pyobjc-framework-Cocoa < 10; sys_platform == 'darwin'
|
||||
pyobjc-framework-LaunchServices < 10; sys_platform == 'darwin'
|
||||
pyobjc-framework-CoreWLAN < 10; sys_platform == 'darwin'
|
||||
tests_require =
|
||||
pytest
|
||||
pytest-qt
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '0.9.1-beta3'
|
||||
__version__ = '0.9.1'
|
||||
|
|
|
@ -308,11 +308,6 @@ class VortaApp(QtSingleApplication):
|
|||
|
||||
Displays a `QMessageBox` with an error message depending on the
|
||||
return code of the `BorgJob`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
repo_url : str
|
||||
The url of the repo of concern
|
||||
"""
|
||||
# extract data from the params for the borg job
|
||||
repo_url = result['params']['repo_url']
|
||||
|
@ -344,7 +339,7 @@ class VortaApp(QtSingleApplication):
|
|||
elif returncode > 128:
|
||||
# 128+N - killed by signal N (e.g. 137 == kill -9)
|
||||
signal = returncode - 128
|
||||
text = self.tr('Repository data check for repo was killed by signal %s.') % (signal)
|
||||
text = self.tr('Repository data check for repo was killed by signal %s.') % signal
|
||||
infotext = self.tr('The process running the check job got a kill signal. Try again.')
|
||||
else:
|
||||
# Real error
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><a href="https://github.com/borgbase/vorta"><span style=" text-decoration: underline; color:#0984e3;">Click here</span></a> for view Git repo.</p></body></html></string>
|
||||
<string><html><head/><body><p><a href="https://github.com/borgbase/vorta"><span style=" text-decoration: underline; color:#0984e3;">Click here</span></a> to view Git repo.</p></body></html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
|
@ -241,7 +241,7 @@
|
|||
<number>20</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<widget class="QLabel" name="copyrightLabel">
|
||||
<property name="text">
|
||||
<string>
|
||||
Vorta is a cross-platform, open-source client designed to simplify the management of Borg backups.
|
||||
|
|
|
@ -626,6 +626,19 @@
|
|||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="logLink">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><a href="file:///"><span style=" text-decoration: underline; color:#0984e3;">View the logs</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_3">
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"patterns":
|
||||
[
|
||||
"fm:*/node_modules",
|
||||
"fm:*/.npm"
|
||||
"fm:*/.npm",
|
||||
"fm:*/npm-global"
|
||||
],
|
||||
"tags": ["type:dev", "lang:javascript", "os:linux", "os:darwin"],
|
||||
"author": "Divi"
|
||||
|
@ -33,5 +34,66 @@
|
|||
],
|
||||
"tags": ["type:dev", "lang:rust", "os:linux", "os:darwin"],
|
||||
"author": "Divi"
|
||||
},
|
||||
{
|
||||
"name": "Visual Studio Code cache and config files",
|
||||
"slug": "vscode-cache",
|
||||
"patterns": [
|
||||
"fm:*/.config/Code",
|
||||
"fm:*/.vscode/extensions/*"
|
||||
],
|
||||
"tags": ["type:editor", "editor:vscode", "os:linux"],
|
||||
"author": "shivansh02"
|
||||
},
|
||||
{
|
||||
"name": "Android Studio Artefacts",
|
||||
"slug": "android-studio",
|
||||
"patterns": [
|
||||
"fm:*/.android",
|
||||
"fm:*/.gradle",
|
||||
"fm:*/Android/Sdk",
|
||||
"fm:*/.AndroidStudio"
|
||||
],
|
||||
"tags": ["type:dev", "editor:android-studio", "os:linux"],
|
||||
"author": "shivansh02"
|
||||
},
|
||||
{
|
||||
"name": "Jetbrains IDEs cache, config, path and logs",
|
||||
"slug": "jetbrains",
|
||||
"patterns": [
|
||||
"fm:*/.config/JetBrains",
|
||||
"fm:*/.cache/JetBrains",
|
||||
"fm:*/.local/share/JetBrains"
|
||||
],
|
||||
"tags": ["type:dev", "editor:jetbrains", "os:linux"],
|
||||
"author": "SAMAD101"
|
||||
},
|
||||
{
|
||||
"name": "AWS artefacts",
|
||||
"slug": "aws-artefacts",
|
||||
"patterns": [
|
||||
"fm:*/.aws"
|
||||
],
|
||||
"tags": ["type:dev", "cloud:aws", "os:linux", "os:darwin"],
|
||||
"author": "SAMAD101"
|
||||
},
|
||||
{
|
||||
"name": "Spotify cache and config files",
|
||||
"slug": "spotify",
|
||||
"patterns": [
|
||||
"fm:*/.cache/spotify",
|
||||
"fm:*/.config/spotify"
|
||||
],
|
||||
"tags": ["type:media", "media:spotify", "os:linux"],
|
||||
"author": "SAMAD101"
|
||||
},
|
||||
{
|
||||
"name": "Docker artefacts",
|
||||
"slug": "docker-artefacts",
|
||||
"patterns": [
|
||||
"fm:*/.docker"
|
||||
],
|
||||
"tags": ["type:dev", "cloud:docker", "os:linux", "os:darwin"],
|
||||
"author": "SAMAD101"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path fill="#000000" d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 768 B |
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.borgbase.Vorta</id>
|
||||
<launchable type="desktop-id">com.borgbase.Vorta.desktop</launchable>
|
||||
<developer_name>Vorta contributors</developer_name>
|
||||
<name>Vorta</name>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
|
@ -40,18 +42,13 @@
|
|||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="v0.9.1-beta3" date="2023-11-30" urgency="low">
|
||||
<release version="v0.9.1" date="2024-01-10" urgency="low">
|
||||
<description>
|
||||
<ul>
|
||||
<li>First production 0.9 release</li>
|
||||
<li>Exclude GUI. By @diivi (#1846)</li>
|
||||
<li>Backup settings.db before migrations. By @AdwaitSalankar (#1848)</li>
|
||||
<li>Loosen platformdirs dependency (#1843)</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="v0.9.1-beta2" date="2023-10-27" urgency="low">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Unit test improvements and coverage increase. By @bigtedde (#1787)</li>
|
||||
<li>Profile sidebar and new setting interface. By @bigtedde (#1809)</li>
|
||||
<li>Update macOS notarization for use with notarytool (#1831)</li>
|
||||
|
|
|
@ -25,9 +25,9 @@ class JobInterface(QObject):
|
|||
@abstractmethod
|
||||
def cancel(self):
|
||||
"""
|
||||
Cancel can be called when the job is not started. It is the responsability of FuncJob to not cancel job if
|
||||
Cancel can be called when the job is not started. It is the responsibility of FuncJob to not cancel job if
|
||||
no job is running.
|
||||
The cancel mehod of JobsManager calls the cancel method on the running jobs only. Other jobs are dequeued.
|
||||
The cancel method of JobsManager calls the cancel method on the running jobs only. Other jobs are dequeued.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -50,6 +50,7 @@ class SiteWorker(threading.Thread):
|
|||
self.current_job = None
|
||||
|
||||
def run(self):
|
||||
job = None
|
||||
while True:
|
||||
try:
|
||||
job = self.jobs.get(False)
|
||||
|
@ -58,7 +59,8 @@ class SiteWorker(threading.Thread):
|
|||
job.run()
|
||||
logger.debug("Finish job for site: %s", job.repo_id())
|
||||
except queue.Empty:
|
||||
logger.debug("No more jobs for site: %s", job.repo_id())
|
||||
if job is not None:
|
||||
logger.debug("No more jobs for site: %s", job.repo_id())
|
||||
return
|
||||
|
||||
|
||||
|
@ -77,19 +79,20 @@ class JobsManager:
|
|||
|
||||
def is_worker_running(self, site=None):
|
||||
"""
|
||||
See if there are any active jobs. The user can't start a backup if a job is
|
||||
running. The scheduler can.
|
||||
"""
|
||||
# Check status for specific site (repo)
|
||||
if site in self.workers:
|
||||
return self.workers[site].is_alive()
|
||||
else:
|
||||
return False
|
||||
See if there are any active jobs.
|
||||
The user can't start a backup if a job is running. The scheduler can.
|
||||
|
||||
# Check if *any* worker is active
|
||||
for _, worker in self.workers.items():
|
||||
if worker.is_alive():
|
||||
return True
|
||||
If site is None, check if there is any worker active for any site (repo).
|
||||
If site is not None, only check if there is a worker active for the given site (repo).
|
||||
"""
|
||||
if site is not None:
|
||||
if site in self.workers:
|
||||
if self.workers[site].is_alive():
|
||||
return True
|
||||
else:
|
||||
for _, worker in self.workers.items():
|
||||
if worker.is_alive():
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_job(self, job):
|
||||
|
|
|
@ -24,7 +24,7 @@ class NetworkStatusMonitor:
|
|||
|
||||
def is_network_status_available(self):
|
||||
"""Is the network status really available, and not just a dummy implementation?"""
|
||||
return type(self) != NetworkStatusMonitor
|
||||
return type(self) is not NetworkStatusMonitor
|
||||
|
||||
def is_network_metered(self) -> bool:
|
||||
"""Is the currently connected network a metered connection?"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import subprocess
|
||||
from datetime import datetime as dt
|
||||
from typing import Iterator, Optional
|
||||
from typing import Iterator, List, Optional
|
||||
|
||||
from CoreWLAN import CWInterface, CWNetwork, CWWiFiClient
|
||||
|
||||
from vorta.log import logger
|
||||
from vorta.network_status.abc import NetworkStatusMonitor, SystemWifiInfo
|
||||
|
@ -8,38 +10,65 @@ from vorta.network_status.abc import NetworkStatusMonitor, SystemWifiInfo
|
|||
|
||||
class DarwinNetworkStatus(NetworkStatusMonitor):
|
||||
def is_network_metered(self) -> bool:
|
||||
return any(is_network_metered(d) for d in get_network_devices())
|
||||
interface: CWInterface = self._get_wifi_interface()
|
||||
network: Optional[CWNetwork] = interface.lastNetworkJoined()
|
||||
|
||||
if network:
|
||||
is_ios_hotspot = network.isPersonalHotspot()
|
||||
else:
|
||||
is_ios_hotspot = False
|
||||
|
||||
return is_ios_hotspot or any(is_network_metered_with_android(d) for d in get_network_devices())
|
||||
|
||||
def get_current_wifi(self) -> Optional[str]:
|
||||
"""
|
||||
Get current SSID or None if Wifi is off.
|
||||
Get current SSID or None if Wi-Fi is off.
|
||||
"""
|
||||
interface: Optional[CWInterface] = self._get_wifi_interface()
|
||||
if not interface:
|
||||
return None
|
||||
|
||||
From https://gist.github.com/keithweaver/00edf356e8194b89ed8d3b7bbead000c
|
||||
"""
|
||||
cmd = [
|
||||
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport',
|
||||
'-I',
|
||||
]
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
out, err = process.communicate()
|
||||
process.wait()
|
||||
for line in out.decode(errors='ignore').split('\n'):
|
||||
split_line = line.strip().split(':')
|
||||
if split_line[0] == 'SSID':
|
||||
return split_line[1].strip()
|
||||
# If the user has Wi-Fi turned off lastNetworkJoined will return None.
|
||||
network: Optional[CWNetwork] = interface.lastNetworkJoined()
|
||||
|
||||
def get_known_wifis(self):
|
||||
if network:
|
||||
network_name = network.ssid()
|
||||
return network_name
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_known_wifis(self) -> List[SystemWifiInfo]:
|
||||
"""
|
||||
Listing all known Wifi networks isn't possible any more from macOS 11. Instead we
|
||||
just return the current Wifi.
|
||||
Use the program, "networksetup", to get the list of know Wi-Fi networks.
|
||||
"""
|
||||
|
||||
wifis = []
|
||||
current_wifi = self.get_current_wifi()
|
||||
if current_wifi is not None:
|
||||
wifis.append(SystemWifiInfo(ssid=current_wifi, last_connected=dt.now()))
|
||||
interface: Optional[CWInterface] = self._get_wifi_interface()
|
||||
if not interface:
|
||||
return []
|
||||
|
||||
interface_name = interface.name()
|
||||
output = call_networksetup_listpreferredwirelessnetworks(interface_name)
|
||||
|
||||
result = []
|
||||
for line in output.strip().splitlines():
|
||||
if line.strip().startswith("Preferred networks"):
|
||||
continue
|
||||
elif not line.strip():
|
||||
continue
|
||||
else:
|
||||
result.append(line.strip())
|
||||
|
||||
for wifi_network_name in result:
|
||||
wifis.append(SystemWifiInfo(ssid=wifi_network_name, last_connected=dt.now()))
|
||||
|
||||
return wifis
|
||||
|
||||
def _get_wifi_interface(self) -> Optional[CWInterface]:
|
||||
wifi_client: CWWiFiClient = CWWiFiClient.sharedWiFiClient()
|
||||
interface: Optional[CWInterface] = wifi_client.interface()
|
||||
return interface
|
||||
|
||||
|
||||
def get_network_devices() -> Iterator[str]:
|
||||
for line in call_networksetup_listallhardwareports().splitlines():
|
||||
|
@ -47,7 +76,7 @@ def get_network_devices() -> Iterator[str]:
|
|||
yield line.split()[1].strip().decode('ascii')
|
||||
|
||||
|
||||
def is_network_metered(bsd_device) -> bool:
|
||||
def is_network_metered_with_android(bsd_device) -> bool:
|
||||
return b'ANDROID_METERED' in call_ipconfig_getpacket(bsd_device)
|
||||
|
||||
|
||||
|
@ -66,3 +95,11 @@ def call_networksetup_listallhardwareports():
|
|||
return subprocess.check_output(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
logger.debug("Command %s failed", ' '.join(cmd))
|
||||
|
||||
|
||||
def call_networksetup_listpreferredwirelessnetworks(interface) -> str:
|
||||
command = ['/usr/sbin/networksetup', '-listpreferredwirelessnetworks', interface]
|
||||
try:
|
||||
return subprocess.check_output(command).decode(encoding='utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
logger.debug("Command %s failed", " ".join(command))
|
||||
|
|
|
@ -36,7 +36,7 @@ class ProfileExport:
|
|||
def repo_url(self):
|
||||
if (
|
||||
'repo' in self._profile_dict
|
||||
and type(self._profile_dict['repo']) == dict
|
||||
and isinstance(self._profile_dict['repo'], dict)
|
||||
and 'url' in self._profile_dict['repo']
|
||||
):
|
||||
return self._profile_dict['repo']['url']
|
||||
|
|
|
@ -70,7 +70,7 @@ class VortaScheduler(QtCore.QObject):
|
|||
self.bus = bus
|
||||
self.bus.connect(service, path, interface, name, "b", self.loginSuspendNotify)
|
||||
else:
|
||||
logger.warn('Failed to connect to DBUS interface to detect sleep/resume events')
|
||||
logger.warning('Failed to connect to DBUS interface to detect sleep/resume events')
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def loginSuspendNotify(self, suspend: bool):
|
||||
|
@ -459,6 +459,7 @@ class VortaScheduler(QtCore.QObject):
|
|||
Pruning and checking after successful backup.
|
||||
"""
|
||||
profile = BackupProfileModel.get(id=profile_id)
|
||||
notifier = VortaNotifications.pick()
|
||||
logger.info('Doing post-backup jobs for %s', profile.name)
|
||||
if profile.prune_on:
|
||||
msg = BorgPruneJob.prepare(profile)
|
||||
|
@ -489,6 +490,11 @@ class VortaScheduler(QtCore.QObject):
|
|||
self.app.jobs_manager.add_job(job)
|
||||
|
||||
logger.info('Finished background task for profile %s', profile.name)
|
||||
notifier.deliver(
|
||||
self.tr('Vorta Backup'),
|
||||
self.tr('Post Backup Tasks successful for %s' % profile.name),
|
||||
level='info',
|
||||
)
|
||||
|
||||
def remove_job(self, profile_id):
|
||||
if profile_id in self.timers:
|
||||
|
|
|
@ -62,7 +62,7 @@ class TrayMenu(QSystemTrayIcon):
|
|||
profiles = BackupProfileModel.select()
|
||||
if profiles.count() > 1:
|
||||
profile_menu = menu.addMenu(self.tr('Backup Now'))
|
||||
for profile in profiles:
|
||||
for profile in sorted(profiles, key=lambda p: (p.name.casefold(), p.name)):
|
||||
new_item = profile_menu.addAction(profile.name)
|
||||
new_item.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i))
|
||||
else:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt6 import QtCore, uic
|
||||
|
||||
|
@ -28,6 +29,9 @@ class AboutTab(AboutTabBase, AboutTabUI, BackupProfileMixin):
|
|||
)
|
||||
self.gpl_logo.setPixmap(get_colored_icon('gpl_logo', scaled_height=40, return_qpixmap=True))
|
||||
self.python_logo.setPixmap(get_colored_icon('python_logo', scaled_height=40, return_qpixmap=True))
|
||||
copyright_text = self.copyrightLabel.text()
|
||||
copyright_text = copyright_text.replace('2020', str(datetime.now().year))
|
||||
self.copyrightLabel.setText(copyright_text)
|
||||
|
||||
def set_borg_details(self, version, path):
|
||||
self.borgVersion.setText(version)
|
||||
|
|
|
@ -875,7 +875,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
|||
return msg.exec() == QMessageBox.StandardButton.Yes
|
||||
|
||||
def delete_action(self):
|
||||
# Since this function modify the UI, we can't put the whole function in a JobQUeue.
|
||||
# Since this function modify the UI, we can't put the whole function in a JobQueue.
|
||||
|
||||
# determine selected archives
|
||||
archives = []
|
||||
|
|
|
@ -190,10 +190,13 @@ class DiffResultDialog(DiffResultBase, DiffResultUI):
|
|||
"""
|
||||
if selection == 0:
|
||||
mode = FileTreeModel.DisplayMode.TREE
|
||||
self.bCollapseAll.setEnabled(True)
|
||||
elif selection == 1:
|
||||
mode = FileTreeModel.DisplayMode.SIMPLIFIED_TREE
|
||||
self.bCollapseAll.setEnabled(True)
|
||||
elif selection == 2:
|
||||
mode = FileTreeModel.DisplayMode.FLAT
|
||||
self.bCollapseAll.setEnabled(False)
|
||||
else:
|
||||
raise Exception("Unknown item in comboBoxDisplayMode with index {}".format(selection))
|
||||
|
||||
|
@ -381,7 +384,6 @@ def parse_diff_lines(lines: List[str], model: 'DiffTree'):
|
|||
|
||||
if not parsed_line:
|
||||
raise Exception("Couldn't parse diff output `{}`".format(line))
|
||||
continue
|
||||
|
||||
path = PurePath(parsed_line['path'])
|
||||
file_type = FileType.FILE
|
||||
|
|
|
@ -185,8 +185,10 @@ class ExtractDialog(ExtractDialogBase, ExtractDialogUI):
|
|||
"""
|
||||
if selection == 0:
|
||||
mode = FileTreeModel.DisplayMode.TREE
|
||||
self.bCollapseAll.setEnabled(True)
|
||||
elif selection == 1:
|
||||
mode = FileTreeModel.DisplayMode.SIMPLIFIED_TREE
|
||||
self.bCollapseAll.setEnabled(True)
|
||||
else:
|
||||
raise Exception("Unknown item in comboBoxDisplayMode with index {}".format(selection))
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
|||
prev_profile_id = SettingsModel.get(key='previous_profile_id')
|
||||
self.current_profile = BackupProfileModel.get_or_none(id=prev_profile_id.str_value)
|
||||
if self.current_profile is None:
|
||||
self.current_profile = BackupProfileModel.select().order_by('name').first()
|
||||
profiles = BackupProfileModel.select()
|
||||
self.current_profile = min(profiles, key=lambda p: (p.name.casefold(), p.name))
|
||||
|
||||
# Load tab models
|
||||
self.repoTab = RepoTab(self.repoTabSlot)
|
||||
|
@ -161,7 +162,8 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
|||
current_item = None
|
||||
|
||||
# Add items to the QListWidget
|
||||
for profile in BackupProfileModel.select().order_by(BackupProfileModel.name):
|
||||
profiles = BackupProfileModel.select()
|
||||
for profile in sorted(profiles, key=lambda p: (p.name.casefold(), p.name)):
|
||||
item = QListWidgetItem(profile.name)
|
||||
item.setData(Qt.ItemDataRole.UserRole, profile.id)
|
||||
|
||||
|
@ -284,13 +286,22 @@ class MainWindow(MainWindowBase, MainWindowUI):
|
|||
def profile_add_edit_result(self, profile_name, profile_id):
|
||||
# Profile is renamed
|
||||
if self.profileSelector.currentItem().data(Qt.ItemDataRole.UserRole) == profile_id:
|
||||
self.profileSelector.currentItem().setText(profile_name)
|
||||
profile = self.profileSelector.takeItem(self.profileSelector.currentRow())
|
||||
profile.setText(profile_name)
|
||||
# Profile is added
|
||||
else:
|
||||
profile = QListWidgetItem(profile_name)
|
||||
profile.setData(Qt.ItemDataRole.UserRole, profile_id)
|
||||
self.profileSelector.addItem(profile)
|
||||
self.profileSelector.setCurrentItem(profile)
|
||||
# Insert the profile at the correct position
|
||||
# Both the casefolded and the original name are used as the key to keep the order stable
|
||||
profile_key = (profile.text().casefold(), profile.text())
|
||||
row = 0
|
||||
for i in range(self.profileSelector.count()):
|
||||
item_name = self.profileSelector.item(i).text()
|
||||
if (item_name.casefold(), item_name) < profile_key:
|
||||
row += 1
|
||||
self.profileSelector.insertItem(row, profile)
|
||||
self.profileSelector.setCurrentItem(profile)
|
||||
|
||||
def toggle_misc_visibility(self):
|
||||
if self.miscWidget.isVisible():
|
||||
|
|
|
@ -170,3 +170,9 @@ class PasswordInput(QObject):
|
|||
self.add_form_to_layout(form_layout)
|
||||
widget.setLayout(form_layout)
|
||||
return widget
|
||||
|
||||
def set_visibility(self, visible: bool) -> None:
|
||||
self._label_password.setVisible(visible)
|
||||
self._label_confirm.setVisible(visible)
|
||||
self.passwordLineEdit.setVisible(visible)
|
||||
self.confirmLineEdit.setVisible(visible)
|
||||
|
|
|
@ -610,7 +610,7 @@ class FileTreeModel(QAbstractItemModel, Generic[T]):
|
|||
if isinstance(path, PurePath):
|
||||
path = path.parts
|
||||
|
||||
return self.root.get_path(path) # handels empty path
|
||||
return self.root.get_path(path) # handles empty path
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
"""
|
||||
|
|
|
@ -27,7 +27,7 @@ class AddProfileWindow(AddProfileBase, AddProfileUI):
|
|||
|
||||
self.name_blank = trans_late('AddProfileWindow', 'Please enter a profile name.')
|
||||
self.name_exists = trans_late('AddProfileWindow', 'A profile with this name already exists.')
|
||||
# Call validate to set inital messages
|
||||
# Call validate to set initial messages
|
||||
self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setEnabled(self.validate())
|
||||
|
||||
def _set_status(self, text):
|
||||
|
|
|
@ -199,8 +199,10 @@ class AddRepoWindow(RepoWindow):
|
|||
'''Validates passwords only if its going to be used'''
|
||||
if self.values['encryption'] == 'none':
|
||||
self.passwordInput.set_validation_enabled(False)
|
||||
self.passwordInput.set_visibility(False)
|
||||
else:
|
||||
self.passwordInput.set_validation_enabled(True)
|
||||
self.passwordInput.set_visibility(True)
|
||||
|
||||
def display_backend_warning(self):
|
||||
'''Display password backend message based off current keyring'''
|
||||
|
|
|
@ -40,7 +40,7 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin):
|
|||
# compression or speed on a unified scale. this is not 1-dimensional and also depends
|
||||
# on the input data. so we just tell what we know for sure.
|
||||
# "auto" is used for some slower / older algorithms to avoid wasting a lot of time
|
||||
# on uncompressible data.
|
||||
# on incompressible data.
|
||||
self.repoCompression.addItem(self.tr('LZ4 (modern, default)'), 'lz4')
|
||||
self.repoCompression.addItem(self.tr('Zstandard Level 3 (modern)'), 'zstd,3')
|
||||
self.repoCompression.addItem(self.tr('Zstandard Level 8 (modern)'), 'zstd,8')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from PyQt6 import QtCore, uic
|
||||
from PyQt6.QtCore import QDateTime, QLocale
|
||||
from PyQt6.QtCore import QDateTime, QLocale, Qt
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
|
@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (
|
|||
QTableWidgetItem,
|
||||
)
|
||||
|
||||
from vorta import application
|
||||
from vorta import application, config
|
||||
from vorta.i18n import get_locale
|
||||
from vorta.scheduler import ScheduleStatusType
|
||||
from vorta.store.models import BackupProfileMixin, EventLogModel, WifiSettingModel
|
||||
|
@ -43,6 +43,10 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
# Set up log table
|
||||
self.logTableWidget.setAlternatingRowColors(True)
|
||||
header = self.logTableWidget.horizontalHeader()
|
||||
self.logLink.setText(
|
||||
f'<a href="file://{config.LOG_DIR}"><span style="text-decoration:'
|
||||
'underline; color:#0984e3;">Click here</span></a> for complete logs.'
|
||||
)
|
||||
header.setVisible(True)
|
||||
[header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) for i in range(5)]
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
|
@ -202,7 +206,7 @@ class ScheduleTab(ScheduleBase, ScheduleUI, BackupProfileMixin):
|
|||
|
||||
def save_wifi_item(self, item):
|
||||
db_item = WifiSettingModel.get(ssid=item.text(), profile=self.profile().id)
|
||||
db_item.allowed = item.checkState() == 2
|
||||
db_item.allowed = item.checkState() == Qt.CheckState.Checked
|
||||
db_item.save()
|
||||
|
||||
def save_profile_attr(self, attr, new_value):
|
||||
|
|
|
@ -331,7 +331,7 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin):
|
|||
profile = self.profile()
|
||||
# sort indexes, starting with lowest
|
||||
indexes.sort()
|
||||
# remove each selected row, starting with highest index (otherways, higher indexes become invalid)
|
||||
# remove each selected row, starting with the highest index (otherwise, higher indexes become invalid)
|
||||
for index in reversed(indexes):
|
||||
db_item = SourceFileModel.get(
|
||||
dir=self.sourceFilesWidget.item(index.row(), SourceColumn.Path).text(),
|
||||
|
|
|
@ -1,25 +1,118 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from vorta.network_status import darwin
|
||||
|
||||
|
||||
def test_get_current_wifi_when_wifi_is_on(mocker):
|
||||
mock_interface = MagicMock()
|
||||
mock_network = MagicMock()
|
||||
mock_interface.lastNetworkJoined.return_value = mock_network
|
||||
mock_network.ssid.return_value = "Coffee Shop Wifi"
|
||||
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
|
||||
|
||||
result = instance.get_current_wifi()
|
||||
|
||||
assert result == "Coffee Shop Wifi"
|
||||
|
||||
|
||||
def test_get_current_wifi_when_wifi_is_off(mocker):
|
||||
mock_interface = MagicMock()
|
||||
mock_interface.lastNetworkJoined.return_value = None
|
||||
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
|
||||
|
||||
result = instance.get_current_wifi()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_current_wifi_when_no_wifi_interface(mocker):
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=None)
|
||||
|
||||
result = instance.get_current_wifi()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_hotspot_enabled", [True, False])
|
||||
def test_network_is_metered_with_ios(mocker, is_hotspot_enabled):
|
||||
mock_interface = MagicMock()
|
||||
mock_network = MagicMock()
|
||||
mock_interface.lastNetworkJoined.return_value = mock_network
|
||||
mock_network.isPersonalHotspot.return_value = is_hotspot_enabled
|
||||
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
|
||||
|
||||
result = instance.is_network_metered()
|
||||
|
||||
assert result == is_hotspot_enabled
|
||||
|
||||
|
||||
def test_network_is_metered_when_wifi_is_off(mocker):
|
||||
mock_interface = MagicMock()
|
||||
mock_interface.lastNetworkJoined.return_value = None
|
||||
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
|
||||
|
||||
result = instance.is_network_metered()
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'getpacket_output_name, expected',
|
||||
[
|
||||
('normal_router', False),
|
||||
('phone', True),
|
||||
('android_phone', True),
|
||||
],
|
||||
)
|
||||
def test_is_network_metered(getpacket_output_name, expected, monkeypatch):
|
||||
def test_is_network_metered_with_android(getpacket_output_name, expected, monkeypatch):
|
||||
def mock_getpacket(device):
|
||||
assert device == 'en0'
|
||||
return GETPACKET_OUTPUTS[getpacket_output_name]
|
||||
|
||||
monkeypatch.setattr(darwin, 'call_ipconfig_getpacket', mock_getpacket)
|
||||
|
||||
result = darwin.is_network_metered('en0')
|
||||
result = darwin.is_network_metered_with_android('en0')
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_get_known_wifi_networks_when_wifi_interface_exists(monkeypatch):
|
||||
networksetup_output = """
|
||||
Preferred networks on en0:
|
||||
Home Network
|
||||
Coffee Shop Wifi
|
||||
iPhone
|
||||
|
||||
Office Wifi
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
darwin, "call_networksetup_listpreferredwirelessnetworks", lambda interface_name: networksetup_output
|
||||
)
|
||||
|
||||
network_status = darwin.DarwinNetworkStatus()
|
||||
result = network_status.get_known_wifis()
|
||||
|
||||
assert len(result) == 4
|
||||
assert result[0].ssid == "Home Network"
|
||||
|
||||
|
||||
def test_get_known_wifi_networks_when_no_wifi_interface(mocker):
|
||||
instance = darwin.DarwinNetworkStatus()
|
||||
mocker.patch.object(instance, "_get_wifi_interface", return_value=None)
|
||||
|
||||
results = instance.get_known_wifis()
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_get_network_devices(monkeypatch):
|
||||
monkeypatch.setattr(darwin, 'call_networksetup_listallhardwareports', lambda: NETWORKSETUP_OUTPUT)
|
||||
|
||||
|
@ -55,7 +148,7 @@ interface_mtu (uint16): 0x5dc
|
|||
server_identifier (ip): 172.16.12.1
|
||||
end (none):
|
||||
""",
|
||||
'phone': b"""\
|
||||
'android_phone': b"""\
|
||||
op = BOOTREPLY
|
||||
htype = 1
|
||||
flags = 0
|
||||
|
|
|
@ -6,14 +6,17 @@ import vorta.utils
|
|||
import vorta.views.archive_tab
|
||||
from PyQt6.QtCore import QDateTime, QItemSelectionModel, Qt
|
||||
from PyQt6.QtWidgets import QMenu
|
||||
from vorta.store.models import ArchiveModel
|
||||
from vorta.views.diff_result import (
|
||||
ChangeType,
|
||||
DiffData,
|
||||
DiffResultDialog,
|
||||
DiffTree,
|
||||
FileType,
|
||||
parse_diff_json,
|
||||
parse_diff_lines,
|
||||
)
|
||||
from vorta.views.partials.treemodel import FileTreeModel
|
||||
|
||||
|
||||
def setup_diff_result_window(qtbot, mocker, tab, borg_json_output, json_mock_file="diff_archives"):
|
||||
|
@ -457,3 +460,19 @@ def test_archive_diff_json_parser(line, expected):
|
|||
|
||||
assert item.path == PurePath(expected[0]).parts
|
||||
assert item.data == DiffData(*expected[1:])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"selection, expected_mode, expected_bCollapseAllEnabled",
|
||||
[
|
||||
(0, FileTreeModel.DisplayMode.TREE, True),
|
||||
(1, FileTreeModel.DisplayMode.SIMPLIFIED_TREE, True),
|
||||
(2, FileTreeModel.DisplayMode.FLAT, False),
|
||||
],
|
||||
)
|
||||
def test_change_display_mode(selection: int, expected_mode, expected_bCollapseAllEnabled):
|
||||
dialog = DiffResultDialog(ArchiveModel(), ArchiveModel(), DiffTree())
|
||||
dialog.change_display_mode(selection)
|
||||
|
||||
assert dialog.model.mode == expected_mode
|
||||
assert dialog.bCollapseAll.isEnabled() == expected_bCollapseAllEnabled
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import pytest
|
||||
import vorta.borg
|
||||
from PyQt6.QtCore import QModelIndex, Qt
|
||||
from vorta.views.extract_dialog import ExtractTree, FileData, FileType, parse_json_lines
|
||||
from vorta.views.partials.treemodel import FileSystemItem
|
||||
from vorta.store.models import ArchiveModel
|
||||
from vorta.views.extract_dialog import (
|
||||
ExtractDialog,
|
||||
ExtractTree,
|
||||
FileData,
|
||||
FileType,
|
||||
parse_json_lines,
|
||||
)
|
||||
from vorta.views.partials.treemodel import FileSystemItem, FileTreeModel
|
||||
|
||||
|
||||
def prepare_borg(mocker, borg_json_output):
|
||||
|
@ -177,3 +185,15 @@ def test_selection():
|
|||
|
||||
select(model, iab)
|
||||
assert a.data.checkstate == Qt.CheckState(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"selection, expected_mode, expected_bCollapseAllEnabled",
|
||||
[(0, FileTreeModel.DisplayMode.TREE, True), (1, FileTreeModel.DisplayMode.SIMPLIFIED_TREE, True)],
|
||||
)
|
||||
def test_change_display_mode(selection: int, expected_mode, expected_bCollapseAllEnabled):
|
||||
dialog = ExtractDialog(ArchiveModel(), ExtractTree())
|
||||
dialog.change_display_mode(selection)
|
||||
|
||||
assert dialog.model.mode == expected_mode
|
||||
assert dialog.bCollapseAll.isEnabled() == expected_bCollapseAllEnabled
|
||||
|
|
|
@ -87,7 +87,7 @@ class TestFileSystemItem:
|
|||
item.add(child2)
|
||||
item.add(child3)
|
||||
|
||||
# test get inexistent subpath
|
||||
# test get nonexistent subpath
|
||||
assert item.get('unknown') is None
|
||||
assert item.get('unknown', default='default') == 'default'
|
||||
|
||||
|
|
Loading…
Reference in New Issue