From 675010e4019fcd7d4fbc43af405a6f93fbec58cb Mon Sep 17 00:00:00 2001 From: TW Date: Tue, 9 Jan 2024 09:06:48 +0100 Subject: [PATCH 1/9] Random cleanups by @ThomasWaldmann (#1879) * fix PEP8 E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()` * remove redundant parentheses * fix SiteWorker.run for empty job queue local variable job is not assigned if queue was empty when calling .run(), but it is used in exception handler. * remove unreachable code in parse_diff_lines * bug fix for unreachable code in is_worker_running the code intended to check if *any* worker is running for any site was *unreachable*. this caused false negative results for site=None. * check_failed_response: remove outdated part of docstring * pull request template: fix relative path to LICENSE.txt * fix typos * use logger.warning, .warn is deprecated --- .github/ISSUE_TEMPLATE/bug_form.yaml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 4 +-- .github/pull_request_template.md | 2 +- noxfile.py | 4 +-- .../fix_app_qt_folder_names_for_codesign.py | 18 +++++------ src/vorta/application.py | 7 +---- src/vorta/borg/jobs_manager.py | 31 ++++++++++--------- src/vorta/network_status/abc.py | 2 +- src/vorta/profile_export.py | 2 +- src/vorta/scheduler.py | 2 +- src/vorta/views/archive_tab.py | 2 +- src/vorta/views/diff_result.py | 1 - src/vorta/views/partials/treemodel.py | 2 +- src/vorta/views/profile_add_edit_dialog.py | 2 +- src/vorta/views/repo_tab.py | 2 +- src/vorta/views/source_tab.py | 2 +- tests/unit/test_treemodel.py | 2 +- 17 files changed, 42 insertions(+), 45 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_form.yaml b/.github/ISSUE_TEMPLATE/bug_form.yaml index de98042ff..f010851a0 100644 --- a/.github/ISSUE_TEMPLATE/bug_form.yaml +++ b/.github/ISSUE_TEMPLATE/bug_form.yaml @@ -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: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b2d69f95..13ec62b3d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d6e05f6a9..231c88ff4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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/ diff --git a/noxfile.py b/noxfile.py index 804b8d60c..b5f6fdfa0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,10 +28,10 @@ @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}") diff --git a/package/fix_app_qt_folder_names_for_codesign.py b/package/fix_app_qt_folder_names_for_codesign.py index 0adfb03f9..cbd5805de 100644 --- a/package/fix_app_qt_folder_names_for_codesign.py +++ b/package/fix_app_qt_folder_names_for_codesign.py @@ -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: diff --git a/src/vorta/application.py b/src/vorta/application.py index 34c1acf91..8857a51eb 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -308,11 +308,6 @@ def check_failed_response(self, result: Dict[str, Any]): 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 @@ def check_failed_response(self, result: Dict[str, Any]): 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 diff --git a/src/vorta/borg/jobs_manager.py b/src/vorta/borg/jobs_manager.py index 2028535d1..4659f3e7f 100644 --- a/src/vorta/borg/jobs_manager.py +++ b/src/vorta/borg/jobs_manager.py @@ -25,9 +25,9 @@ def repo_id(self): @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 @@ def __init__(self, jobs): self.current_job = None def run(self): + job = None while True: try: job = self.jobs.get(False) @@ -58,7 +59,8 @@ def run(self): 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 @@ def __init__(self): 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. + See if there are any active jobs. + The user can't start a backup if a job is running. The scheduler can. + + 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). """ - # Check status for specific site (repo) - if site in self.workers: - return self.workers[site].is_alive() + if site is not None: + if site in self.workers: + if self.workers[site].is_alive(): + return True else: - return False - - # Check if *any* worker is active - for _, worker in self.workers.items(): - if worker.is_alive(): - return True + for _, worker in self.workers.items(): + if worker.is_alive(): + return True return False def add_job(self, job): diff --git a/src/vorta/network_status/abc.py b/src/vorta/network_status/abc.py index 60f9353ac..7f74fc16b 100644 --- a/src/vorta/network_status/abc.py +++ b/src/vorta/network_status/abc.py @@ -24,7 +24,7 @@ def get_network_status_monitor(cls) -> '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?""" diff --git a/src/vorta/profile_export.py b/src/vorta/profile_export.py index fa26ac5c6..a370ce1d7 100644 --- a/src/vorta/profile_export.py +++ b/src/vorta/profile_export.py @@ -36,7 +36,7 @@ def schema_version(self): 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'] diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index 7a3fcee5d..aa8b2859c 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -70,7 +70,7 @@ def __init__(self): 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): diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index cb4d41843..d2af5757b 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -875,7 +875,7 @@ def confirm_dialog(self, title, text): 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 = [] diff --git a/src/vorta/views/diff_result.py b/src/vorta/views/diff_result.py index 5d262efa5..da74ff727 100644 --- a/src/vorta/views/diff_result.py +++ b/src/vorta/views/diff_result.py @@ -381,7 +381,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 diff --git a/src/vorta/views/partials/treemodel.py b/src/vorta/views/partials/treemodel.py index a184a5428..ceff3eb46 100644 --- a/src/vorta/views/partials/treemodel.py +++ b/src/vorta/views/partials/treemodel.py @@ -610,7 +610,7 @@ def getItem(self, path: Union[PurePath, PathLike]) -> Optional[FileSystemItem[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): """ diff --git a/src/vorta/views/profile_add_edit_dialog.py b/src/vorta/views/profile_add_edit_dialog.py index 56d040c1a..75b767d91 100644 --- a/src/vorta/views/profile_add_edit_dialog.py +++ b/src/vorta/views/profile_add_edit_dialog.py @@ -27,7 +27,7 @@ def __init__(self, parent=None): 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): diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index 3b04ffa23..e9f38dbad 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -40,7 +40,7 @@ def __init__(self, parent=None): # 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') diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 5a839e62f..ed63be2a7 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -331,7 +331,7 @@ def source_remove(self): 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(), diff --git a/tests/unit/test_treemodel.py b/tests/unit/test_treemodel.py index 1b76d5856..dd2b9717e 100644 --- a/tests/unit/test_treemodel.py +++ b/tests/unit/test_treemodel.py @@ -87,7 +87,7 @@ def test_get(self): 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' From be6e08552abb8c93433f6e0aeea0bd2481362b63 Mon Sep 17 00:00:00 2001 From: Manu <3916435+m3nu@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:11:38 +0000 Subject: [PATCH 2/9] Update screencast for v0.9 (#1881) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59ddbf9fc..b58d1e5e4 100644 --- a/README.md +++ b/README.md @@ -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? 🤩 From 1d85cb48dcd1b08c1bc9c26b09a85550afeaebcb Mon Sep 17 00:00:00 2001 From: Manu Date: Wed, 10 Jan 2024 13:20:01 +0000 Subject: [PATCH 3/9] Bump version to v0.9.1 --- src/vorta/_version.py | 2 +- src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vorta/_version.py b/src/vorta/_version.py index 55616197f..8969d4966 100644 --- a/src/vorta/_version.py +++ b/src/vorta/_version.py @@ -1 +1 @@ -__version__ = '0.9.1-beta3' +__version__ = '0.9.1' diff --git a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml index 5c11d044e..24b878566 100644 --- a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml +++ b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml @@ -49,6 +49,13 @@ + + +
    +
  • First production 0.9 release
  • +
+
+
    From 9cc7a98838d2f6bbcf984dd03ac35994b2c62340 Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 11 Jan 2024 08:27:25 +0000 Subject: [PATCH 4/9] Minor: color settings icon --- src/vorta/assets/icons/settings_wheel.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vorta/assets/icons/settings_wheel.svg b/src/vorta/assets/icons/settings_wheel.svg index 326c4c686..05295e599 100644 --- a/src/vorta/assets/icons/settings_wheel.svg +++ b/src/vorta/assets/icons/settings_wheel.svg @@ -1 +1 @@ - + From 466597207666996629e2f49cbd5482760b84c38b Mon Sep 17 00:00:00 2001 From: Manu <3916435+m3nu@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:26:06 +0000 Subject: [PATCH 5/9] Fix issue after Qt6 migration to save allowed Wifis (#1903) --- src/vorta/views/schedule_tab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index 43de6730d..a03b191c7 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -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, @@ -202,7 +202,7 @@ def populate_wifi(self): 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): From 0cc15e3d3d647bae1782f2c21eafacbf2c8073c6 Mon Sep 17 00:00:00 2001 From: Hofer-Julian <30049909+Hofer-Julian@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:06:56 +0100 Subject: [PATCH 6/9] Update appdata.xml (#1885) The appdata.xml doesn't pass validation of flathub 1. The `launchable` tag is nowadays required 2. Flatpak doesn't like the beta releases. In the end, it only made sense to remove them from the xml --- .../metadata/com.borgbase.Vorta.appdata.xml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml index 24b878566..e24277ad2 100644 --- a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml +++ b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml @@ -1,6 +1,7 @@ com.borgbase.Vorta + com.borgbase.Vorta.desktop Vorta GPL-3.0 CC0-1.0 @@ -40,25 +41,13 @@ - +
      +
    • First production 0.9 release
    • Exclude GUI. By @diivi (#1846)
    • Backup settings.db before migrations. By @AdwaitSalankar (#1848)
    • Loosen platformdirs dependency (#1843)
    • -
    -
    -
    - - -
      -
    • First production 0.9 release
    • -
    -
    -
    - - -
    • Unit test improvements and coverage increase. By @bigtedde (#1787)
    • Profile sidebar and new setting interface. By @bigtedde (#1809)
    • Update macOS notarization for use with notarytool (#1831)
    • From 634f984e78ea261825049b2617e62ecf31503b73 Mon Sep 17 00:00:00 2001 From: Jeff Ramnani Date: Fri, 2 Feb 2024 04:05:47 -0800 Subject: [PATCH 7/9] Improve metered connection detection for macOS. By @jramnani (#1902) * Add dependency for pyobjc-CoreWLAN on darwin * Rename existing implementation with Android The current implementation was tested with Android, but does not work with iOS. Move the existing implementation and include android in the name to make room for adding a new iOS metered connection detection strategy. * get_current_wifi works with objc Switch from using command line tools to using the Objective-C Cocoa API to get the Wi-Fi status information. Cocoa has an API to specifically check whether a Wi-Fi connection is using a Personal Hotspot on iOS. I'm using a private method to get the Wi-Fi interface object in Cocoa. The reason for this is that cleaning up mocks on PyObjC/ObjC objects is much harder than mocking out methods on objects in our control. Using test doubles also let's me check for different states the Wi-Fi network could be in. * get_known_wifis works on darwin Use the networksetup command on macOS to get the list of the user's Wi-Fi networks. networksetup -listpreferredwirelessnetworks bsd_device It looks like this command and option has existed on macOS since at least 2013. Also add some type annotations around the PyObjC return values to help the reader know what they're dealing with at each step. * Add test for get_current_wifi when wifi is off The user might have Wi-Fi turned off. Account for that use case. * Add iOS Personal Hotspot support to is_network_metered The DarwinNetworkManager can now determine if the user is connected to a Personal Hotspot Wi-Fi network from iOS. Account for whether the user has Wi-Fi turned on and off. * Refactor to avoid deprecated API in Cocoa According to Apple's developer documentation, creating CWInterface objects directly are discouraged. Instead, they prefer to use CWInterface objects created by CWWiFiClient. This also happens to be more compliant with Apple's application sandbox. Creating CWInterface objects directly accesses raw BSD sockets which is not allowed in the sandbox. More details here: https://developer.apple.com/documentation/corewlan/cwinterface * Add test case for blank Wi-Fi network name I have one of these in my list of networks in Vorta. And this also covers a missing branch in get_known_wifis. * Move private method below public methods This is to provide a little more clarity. Especially since this class is subclassing another one. * Account for when there is no wifi interface When a Mac does not have a Wi-Fi interface, CWWiFiClient.interface() can return None. Update the type annotation to mark it as Optional, and account for the null condition in the other methods. * Fix type annotation error The CI tests failed on python 3.8. I used the wrong type annotation to describe a list of SystemWifiInfo's. The tests now pass for me when I run 'make test-unit' using a python 3.8 interpreter. * Fix linter issue with imports --- setup.cfg | 1 + src/vorta/network_status/darwin.py | 85 +++++++++++++++------- tests/network_manager/test_darwin.py | 101 +++++++++++++++++++++++++-- 3 files changed, 159 insertions(+), 28 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6cda372a8..e0cdd9319 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/src/vorta/network_status/darwin.py b/src/vorta/network_status/darwin.py index 279fc13aa..1ee2baf11 100644 --- a/src/vorta/network_status/darwin.py +++ b/src/vorta/network_status/darwin.py @@ -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 @@ 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. - - From https://gist.github.com/keithweaver/00edf356e8194b89ed8d3b7bbead000c + Get current SSID or None if Wi-Fi is off. """ - 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() - - def get_known_wifis(self): + interface: Optional[CWInterface] = self._get_wifi_interface() + if not interface: + return None + + # If the user has Wi-Fi turned off lastNetworkJoined will return None. + network: Optional[CWNetwork] = interface.lastNetworkJoined() + + 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)) diff --git a/tests/network_manager/test_darwin.py b/tests/network_manager/test_darwin.py index 70c96cd2e..7d900dd44 100644 --- a/tests/network_manager/test_darwin.py +++ b/tests/network_manager/test_darwin.py @@ -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 @@ def test_get_network_devices(monkeypatch): server_identifier (ip): 172.16.12.1 end (none): """, - 'phone': b"""\ + 'android_phone': b"""\ op = BOOTREPLY htype = 1 flags = 0 From d8cce255eb5b1e924608504dc630a3c788d8e5cd Mon Sep 17 00:00:00 2001 From: Hofer-Julian <30049909+Hofer-Julian@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:29:14 +0100 Subject: [PATCH 8/9] Add developer name to appdata (#1922) * Add developer name to appdata Flathub is getting more and more strict when it comes to metadata. I've added "Vorta developers" no, I can also be more specific if people prefer that. * Update com.borgbase.Vorta.appdata.xml --- src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml index e24277ad2..b0c8a76b0 100644 --- a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml +++ b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml @@ -2,6 +2,7 @@ com.borgbase.Vorta com.borgbase.Vorta.desktop + Vorta contributors Vorta GPL-3.0 CC0-1.0 From 472c7c8996c2744ef3ed70bf5cad44c08fa1c582 Mon Sep 17 00:00:00 2001 From: Shivansh Singh <89853707+shivansh02@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:05:33 +0530 Subject: [PATCH 9/9] Fix About dialog wording and year. By @shivansh02 (#1936) * fix: about dialogue grammar and copyright year * fix: made about dialogue copyright year dynamic --- src/vorta/assets/UI/abouttab.ui | 4 ++-- src/vorta/views/about_tab.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vorta/assets/UI/abouttab.ui b/src/vorta/assets/UI/abouttab.ui index 791b72915..52f40f7b1 100644 --- a/src/vorta/assets/UI/abouttab.ui +++ b/src/vorta/assets/UI/abouttab.ui @@ -213,7 +213,7 @@ - <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> + <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> true @@ -241,7 +241,7 @@ 20 - + Vorta is a cross-platform, open-source client designed to simplify the management of Borg backups. diff --git a/src/vorta/views/about_tab.py b/src/vorta/views/about_tab.py index 41928b400..da6d791a2 100644 --- a/src/vorta/views/about_tab.py +++ b/src/vorta/views/about_tab.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from PyQt6 import QtCore, uic @@ -28,6 +29,9 @@ def __init__(self, parent=None): ) 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)