mirror of https://github.com/borgbase/vorta
Merge branch 'master' into sort-profiles-tray
This commit is contained in:
commit
6b1eb5747b
|
@ -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
|
||||
|
|
|
@ -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,27 @@
|
|||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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,25 +42,13 @@
|
|||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="v0.9.1-beta3" date="2023-11-30" urgency="low">
|
||||
<description>
|
||||
<ul>
|
||||
<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" date="2024-01-10" urgency="low">
|
||||
<description>
|
||||
<ul>
|
||||
<li>First production 0.9 release</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="v0.9.1-beta2" date="2023-10-27" urgency="low">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Exclude GUI. By @diivi (#1846)</li>
|
||||
<li>Backup settings.db before migrations. By @AdwaitSalankar (#1848)</li>
|
||||
<li>Loosen platformdirs dependency (#1843)</li>
|
||||
<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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue