Merge branch 'master' into sort-profiles-tray

This commit is contained in:
yfprojects 2024-04-01 14:23:53 +00:00 committed by GitHub
commit 6b1eb5747b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 213 additions and 48 deletions

View File

@ -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

View File

@ -213,7 +213,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/borgbase/vorta&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0984e3;&quot;&gt;Click here&lt;/span&gt;&lt;/a&gt; for view Git repo.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/borgbase/vorta&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0984e3;&quot;&gt;Click here&lt;/span&gt;&lt;/a&gt; to view Git repo.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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.

View File

@ -626,6 +626,19 @@
</column>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="logLink">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;file:///&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0984e3;&quot;&gt;View the logs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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">

View File

@ -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"
}
]

View File

@ -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>

View File

@ -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))

View File

@ -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)

View File

@ -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):

View File

@ -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