Skip to content

Commit 88cb2dc

Browse files
authored
Merge branch 'borgbase:master' into issue1799
2 parents d332042 + b2cf5b1 commit 88cb2dc

File tree

8 files changed

+189
-47
lines changed

8 files changed

+189
-47
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ install_requires =
4747
pyobjc-core < 10; sys_platform == 'darwin'
4848
pyobjc-framework-Cocoa < 10; sys_platform == 'darwin'
4949
pyobjc-framework-LaunchServices < 10; sys_platform == 'darwin'
50+
pyobjc-framework-CoreWLAN < 10; sys_platform == 'darwin'
5051
tests_require =
5152
pytest
5253
pytest-qt

src/vorta/assets/UI/abouttab.ui

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@
213213
<item>
214214
<widget class="QLabel" name="label">
215215
<property name="text">
216-
<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>
216+
<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>
217217
</property>
218218
<property name="openExternalLinks">
219219
<bool>true</bool>
@@ -241,7 +241,7 @@
241241
<number>20</number>
242242
</property>
243243
<item>
244-
<widget class="QLabel" name="label">
244+
<widget class="QLabel" name="copyrightLabel">
245245
<property name="text">
246246
<string>
247247
Vorta is a cross-platform, open-source client designed to simplify the management of Borg backups.

src/vorta/assets/UI/scheduletab.ui

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,19 @@
626626
</column>
627627
</widget>
628628
</item>
629+
<item row="2" column="0">
630+
<widget class="QLabel" name="logLink">
631+
<property name="text">
632+
<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>
633+
</property>
634+
<property name="indent">
635+
<number>0</number>
636+
</property>
637+
<property name="openExternalLinks">
638+
<bool>true</bool>
639+
</property>
640+
</widget>
641+
</item>
629642
</layout>
630643
</widget>
631644
<widget class="QWidget" name="page_3">

src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<component type="desktop-application">
33
<id>com.borgbase.Vorta</id>
4+
<launchable type="desktop-id">com.borgbase.Vorta.desktop</launchable>
5+
<developer_name>Vorta contributors</developer_name>
46
<name>Vorta</name>
57
<project_license>GPL-3.0</project_license>
68
<metadata_license>CC0-1.0</metadata_license>
@@ -40,25 +42,13 @@
4042
</screenshot>
4143
</screenshots>
4244
<releases>
43-
<release version="v0.9.1-beta3" date="2023-11-30" urgency="low">
45+
<release version="v0.9.1" date="2024-01-10" urgency="low">
4446
<description>
4547
<ul>
48+
<li>First production 0.9 release</li>
4649
<li>Exclude GUI. By @diivi (#1846)</li>
4750
<li>Backup settings.db before migrations. By @AdwaitSalankar (#1848)</li>
4851
<li>Loosen platformdirs dependency (#1843)</li>
49-
</ul>
50-
</description>
51-
</release>
52-
<release version="v0.9.1" date="2024-01-10" urgency="low">
53-
<description>
54-
<ul>
55-
<li>First production 0.9 release</li>
56-
</ul>
57-
</description>
58-
</release>
59-
<release version="v0.9.1-beta2" date="2023-10-27" urgency="low">
60-
<description>
61-
<ul>
6252
<li>Unit test improvements and coverage increase. By @bigtedde (#1787)</li>
6353
<li>Profile sidebar and new setting interface. By @bigtedde (#1809)</li>
6454
<li>Update macOS notarization for use with notarytool (#1831)</li>

src/vorta/network_status/darwin.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,82 @@
11
import subprocess
22
from datetime import datetime as dt
3-
from typing import Iterator, Optional
3+
from typing import Iterator, List, Optional
4+
5+
from CoreWLAN import CWInterface, CWNetwork, CWWiFiClient
46

57
from vorta.log import logger
68
from vorta.network_status.abc import NetworkStatusMonitor, SystemWifiInfo
79

810

911
class DarwinNetworkStatus(NetworkStatusMonitor):
1012
def is_network_metered(self) -> bool:
11-
return any(is_network_metered(d) for d in get_network_devices())
13+
interface: CWInterface = self._get_wifi_interface()
14+
network: Optional[CWNetwork] = interface.lastNetworkJoined()
15+
16+
if network:
17+
is_ios_hotspot = network.isPersonalHotspot()
18+
else:
19+
is_ios_hotspot = False
20+
21+
return is_ios_hotspot or any(is_network_metered_with_android(d) for d in get_network_devices())
1222

1323
def get_current_wifi(self) -> Optional[str]:
1424
"""
15-
Get current SSID or None if Wifi is off.
16-
17-
From https://gist.github.com/keithweaver/00edf356e8194b89ed8d3b7bbead000c
25+
Get current SSID or None if Wi-Fi is off.
1826
"""
19-
cmd = [
20-
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport',
21-
'-I',
22-
]
23-
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
24-
out, err = process.communicate()
25-
process.wait()
26-
for line in out.decode(errors='ignore').split('\n'):
27-
split_line = line.strip().split(':')
28-
if split_line[0] == 'SSID':
29-
return split_line[1].strip()
30-
31-
def get_known_wifis(self):
27+
interface: Optional[CWInterface] = self._get_wifi_interface()
28+
if not interface:
29+
return None
30+
31+
# If the user has Wi-Fi turned off lastNetworkJoined will return None.
32+
network: Optional[CWNetwork] = interface.lastNetworkJoined()
33+
34+
if network:
35+
network_name = network.ssid()
36+
return network_name
37+
else:
38+
return None
39+
40+
def get_known_wifis(self) -> List[SystemWifiInfo]:
3241
"""
33-
Listing all known Wifi networks isn't possible any more from macOS 11. Instead we
34-
just return the current Wifi.
42+
Use the program, "networksetup", to get the list of know Wi-Fi networks.
3543
"""
44+
3645
wifis = []
37-
current_wifi = self.get_current_wifi()
38-
if current_wifi is not None:
39-
wifis.append(SystemWifiInfo(ssid=current_wifi, last_connected=dt.now()))
46+
interface: Optional[CWInterface] = self._get_wifi_interface()
47+
if not interface:
48+
return []
49+
50+
interface_name = interface.name()
51+
output = call_networksetup_listpreferredwirelessnetworks(interface_name)
52+
53+
result = []
54+
for line in output.strip().splitlines():
55+
if line.strip().startswith("Preferred networks"):
56+
continue
57+
elif not line.strip():
58+
continue
59+
else:
60+
result.append(line.strip())
61+
62+
for wifi_network_name in result:
63+
wifis.append(SystemWifiInfo(ssid=wifi_network_name, last_connected=dt.now()))
4064

4165
return wifis
4266

67+
def _get_wifi_interface(self) -> Optional[CWInterface]:
68+
wifi_client: CWWiFiClient = CWWiFiClient.sharedWiFiClient()
69+
interface: Optional[CWInterface] = wifi_client.interface()
70+
return interface
71+
4372

4473
def get_network_devices() -> Iterator[str]:
4574
for line in call_networksetup_listallhardwareports().splitlines():
4675
if line.startswith(b'Device: '):
4776
yield line.split()[1].strip().decode('ascii')
4877

4978

50-
def is_network_metered(bsd_device) -> bool:
79+
def is_network_metered_with_android(bsd_device) -> bool:
5180
return b'ANDROID_METERED' in call_ipconfig_getpacket(bsd_device)
5281

5382

@@ -66,3 +95,11 @@ def call_networksetup_listallhardwareports():
6695
return subprocess.check_output(cmd)
6796
except subprocess.CalledProcessError:
6897
logger.debug("Command %s failed", ' '.join(cmd))
98+
99+
100+
def call_networksetup_listpreferredwirelessnetworks(interface) -> str:
101+
command = ['/usr/sbin/networksetup', '-listpreferredwirelessnetworks', interface]
102+
try:
103+
return subprocess.check_output(command).decode(encoding='utf-8')
104+
except subprocess.CalledProcessError:
105+
logger.debug("Command %s failed", " ".join(command))

src/vorta/views/about_tab.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import datetime
23

34
from PyQt6 import QtCore, uic
45

@@ -28,6 +29,9 @@ def __init__(self, parent=None):
2829
)
2930
self.gpl_logo.setPixmap(get_colored_icon('gpl_logo', scaled_height=40, return_qpixmap=True))
3031
self.python_logo.setPixmap(get_colored_icon('python_logo', scaled_height=40, return_qpixmap=True))
32+
copyright_text = self.copyrightLabel.text()
33+
copyright_text = copyright_text.replace('2020', str(datetime.now().year))
34+
self.copyrightLabel.setText(copyright_text)
3135

3236
def set_borg_details(self, version, path):
3337
self.borgVersion.setText(version)

src/vorta/views/schedule_tab.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from PyQt6 import QtCore, uic
2-
from PyQt6.QtCore import QDateTime, QLocale
2+
from PyQt6.QtCore import QDateTime, QLocale, Qt
33
from PyQt6.QtWidgets import (
44
QAbstractItemView,
55
QApplication,
@@ -8,7 +8,7 @@
88
QTableWidgetItem,
99
)
1010

11-
from vorta import application
11+
from vorta import application, config
1212
from vorta.i18n import get_locale
1313
from vorta.scheduler import ScheduleStatusType
1414
from vorta.store.models import BackupProfileMixin, EventLogModel, WifiSettingModel
@@ -43,6 +43,10 @@ def __init__(self, parent=None):
4343
# Set up log table
4444
self.logTableWidget.setAlternatingRowColors(True)
4545
header = self.logTableWidget.horizontalHeader()
46+
self.logLink.setText(
47+
f'<a href="file://{config.LOG_DIR}"><span style="text-decoration:'
48+
'underline; color:#0984e3;">Click here</span></a> for complete logs.'
49+
)
4650
header.setVisible(True)
4751
[header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) for i in range(5)]
4852
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
@@ -202,7 +206,7 @@ def populate_wifi(self):
202206

203207
def save_wifi_item(self, item):
204208
db_item = WifiSettingModel.get(ssid=item.text(), profile=self.profile().id)
205-
db_item.allowed = item.checkState() == 2
209+
db_item.allowed = item.checkState() == Qt.CheckState.Checked
206210
db_item.save()
207211

208212
def save_profile_attr(self, attr, new_value):

tests/network_manager/test_darwin.py

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,118 @@
1+
from unittest.mock import MagicMock
2+
13
import pytest
24
from vorta.network_status import darwin
35

46

7+
def test_get_current_wifi_when_wifi_is_on(mocker):
8+
mock_interface = MagicMock()
9+
mock_network = MagicMock()
10+
mock_interface.lastNetworkJoined.return_value = mock_network
11+
mock_network.ssid.return_value = "Coffee Shop Wifi"
12+
13+
instance = darwin.DarwinNetworkStatus()
14+
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
15+
16+
result = instance.get_current_wifi()
17+
18+
assert result == "Coffee Shop Wifi"
19+
20+
21+
def test_get_current_wifi_when_wifi_is_off(mocker):
22+
mock_interface = MagicMock()
23+
mock_interface.lastNetworkJoined.return_value = None
24+
25+
instance = darwin.DarwinNetworkStatus()
26+
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
27+
28+
result = instance.get_current_wifi()
29+
30+
assert result is None
31+
32+
33+
def test_get_current_wifi_when_no_wifi_interface(mocker):
34+
instance = darwin.DarwinNetworkStatus()
35+
mocker.patch.object(instance, "_get_wifi_interface", return_value=None)
36+
37+
result = instance.get_current_wifi()
38+
39+
assert result is None
40+
41+
42+
@pytest.mark.parametrize("is_hotspot_enabled", [True, False])
43+
def test_network_is_metered_with_ios(mocker, is_hotspot_enabled):
44+
mock_interface = MagicMock()
45+
mock_network = MagicMock()
46+
mock_interface.lastNetworkJoined.return_value = mock_network
47+
mock_network.isPersonalHotspot.return_value = is_hotspot_enabled
48+
49+
instance = darwin.DarwinNetworkStatus()
50+
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
51+
52+
result = instance.is_network_metered()
53+
54+
assert result == is_hotspot_enabled
55+
56+
57+
def test_network_is_metered_when_wifi_is_off(mocker):
58+
mock_interface = MagicMock()
59+
mock_interface.lastNetworkJoined.return_value = None
60+
61+
instance = darwin.DarwinNetworkStatus()
62+
mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface)
63+
64+
result = instance.is_network_metered()
65+
66+
assert result is False
67+
68+
569
@pytest.mark.parametrize(
670
'getpacket_output_name, expected',
771
[
872
('normal_router', False),
9-
('phone', True),
73+
('android_phone', True),
1074
],
1175
)
12-
def test_is_network_metered(getpacket_output_name, expected, monkeypatch):
76+
def test_is_network_metered_with_android(getpacket_output_name, expected, monkeypatch):
1377
def mock_getpacket(device):
1478
assert device == 'en0'
1579
return GETPACKET_OUTPUTS[getpacket_output_name]
1680

1781
monkeypatch.setattr(darwin, 'call_ipconfig_getpacket', mock_getpacket)
1882

19-
result = darwin.is_network_metered('en0')
83+
result = darwin.is_network_metered_with_android('en0')
2084
assert result == expected
2185

2286

87+
def test_get_known_wifi_networks_when_wifi_interface_exists(monkeypatch):
88+
networksetup_output = """
89+
Preferred networks on en0:
90+
Home Network
91+
Coffee Shop Wifi
92+
iPhone
93+
94+
Office Wifi
95+
"""
96+
monkeypatch.setattr(
97+
darwin, "call_networksetup_listpreferredwirelessnetworks", lambda interface_name: networksetup_output
98+
)
99+
100+
network_status = darwin.DarwinNetworkStatus()
101+
result = network_status.get_known_wifis()
102+
103+
assert len(result) == 4
104+
assert result[0].ssid == "Home Network"
105+
106+
107+
def test_get_known_wifi_networks_when_no_wifi_interface(mocker):
108+
instance = darwin.DarwinNetworkStatus()
109+
mocker.patch.object(instance, "_get_wifi_interface", return_value=None)
110+
111+
results = instance.get_known_wifis()
112+
113+
assert results == []
114+
115+
23116
def test_get_network_devices(monkeypatch):
24117
monkeypatch.setattr(darwin, 'call_networksetup_listallhardwareports', lambda: NETWORKSETUP_OUTPUT)
25118

@@ -55,7 +148,7 @@ def test_get_network_devices(monkeypatch):
55148
server_identifier (ip): 172.16.12.1
56149
end (none):
57150
""",
58-
'phone': b"""\
151+
'android_phone': b"""\
59152
op = BOOTREPLY
60153
htype = 1
61154
flags = 0

0 commit comments

Comments
 (0)