From a207b90bdffac5587f44358b709e33010259dcff Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 13:21:11 -0300 Subject: [PATCH 01/46] new desktop entries using Linux standards --- aur/bauh.desktop | 9 +++++++++ aur/bauh_tray.desktop | 9 +++++++++ aur/panel_entry.py | 24 ------------------------ aur/tray_entry.py | 31 ------------------------------- 4 files changed, 18 insertions(+), 55 deletions(-) create mode 100644 aur/bauh.desktop create mode 100644 aur/bauh_tray.desktop delete mode 100644 aur/panel_entry.py delete mode 100644 aur/tray_entry.py diff --git a/aur/bauh.desktop b/aur/bauh.desktop new file mode 100644 index 00000000..d4cabde6 --- /dev/null +++ b/aur/bauh.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=bauh +Categories=System; +Comment=Manage your Flatpak / Snap / AppImage / AUR applications +Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR +Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR +Exec=/usr/bin/bauh +Icon=bauh \ No newline at end of file diff --git a/aur/bauh_tray.desktop b/aur/bauh_tray.desktop new file mode 100644 index 00000000..9b4f325f --- /dev/null +++ b/aur/bauh_tray.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=bauh +Categories=System; +Comment=Manage your Flatpak / Snap / AppImage / AUR applications +Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR +Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR +Exec=/usr/bin/bauh --tray=1 +Icon=bauh \ No newline at end of file diff --git a/aur/panel_entry.py b/aur/panel_entry.py deleted file mode 100644 index 3a37336d..00000000 --- a/aur/panel_entry.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generates a .desktop file based on the current python version. Used for AUR installation -import os -import sys - -desktop_file = """ -[Desktop Entry] -Type=Application -Name=bauh -Categories=System; -Comment=Manage your Flatpak / Snap / AppImage / AUR applications -Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR -Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR -Exec = {path} -Icon = {lib_path}/python{version}/site-packages/bauh/view/resources/img/logo.svg -""" - -py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor) - -app_cmd = os.getenv('BAUH_PATH', '/usr/bin/bauh') - -with open('bauh.desktop', 'w+') as f: - f.write(desktop_file.format(lib_path=os.getenv('BAUH_LIB_PATH', '/usr/lib'), - version=py_version, - path=app_cmd)) diff --git a/aur/tray_entry.py b/aur/tray_entry.py deleted file mode 100644 index 109bc597..00000000 --- a/aur/tray_entry.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generates a .desktop file based on the current python version. Used for AUR installation -import os -import sys - - -desktop_file = """ -[Desktop Entry] -Type = Application -Name = bauh ( tray ) -Name[pt] = bauh ( bandeja ) -Name[es] = bauh ( bandeja ) -Categories = System; -Comment=Manage your Flatpak / Snap / AppImage / AUR applications -Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR -Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR -Exec = {path} -Icon = {lib_path}/python{version}/site-packages/bauh/view/resources/img/logo.svg -""" - -py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor) - -app_cmd = os.getenv('BAUH_PATH', '/usr/bin/bauh') + ' --tray=1' - -with open('bauh_tray.desktop', 'w+') as f: - f.write(desktop_file.format(lib_path=os.getenv('BAUH_LIB_PATH', '/usr/lib'), - version=py_version, - path=app_cmd)) - - -with open('bauh-tray', 'w') as f: - f.write(app_cmd) From 6bde54fa20fb76e93d9764dd2e4adac4a616c9f3 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 13:27:45 -0300 Subject: [PATCH 02/46] aur dist .svg icon --- aur/bauh.svg | 477 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 aur/bauh.svg diff --git a/aur/bauh.svg b/aur/bauh.svg new file mode 100644 index 00000000..2714f294 --- /dev/null +++ b/aur/bauh.svg @@ -0,0 +1,477 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Layer 1 \ No newline at end of file From 0e371c3cf8efe25f800aabef2758dbe6e29b9899 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 13:53:56 -0300 Subject: [PATCH 03/46] aur dist icons for several resolutions --- aur/bauh.svg | 477 ------------------------------------------ aur/icons/128x128.png | Bin 0 -> 6580 bytes aur/icons/12x12.png | Bin 0 -> 889 bytes aur/icons/16x16.png | Bin 0 -> 1011 bytes aur/icons/24x24.png | Bin 0 -> 1433 bytes aur/icons/256x256.png | Bin 0 -> 9831 bytes aur/icons/32x32.png | Bin 0 -> 1667 bytes aur/icons/48x48.png | Bin 0 -> 2745 bytes aur/icons/512x512.png | Bin 0 -> 22325 bytes aur/icons/64x64.png | Bin 0 -> 2983 bytes aur/icons/8x8.png | Bin 0 -> 726 bytes aur/icons/96x96.png | Bin 0 -> 4429 bytes 12 files changed, 477 deletions(-) delete mode 100644 aur/bauh.svg create mode 100644 aur/icons/128x128.png create mode 100644 aur/icons/12x12.png create mode 100644 aur/icons/16x16.png create mode 100644 aur/icons/24x24.png create mode 100644 aur/icons/256x256.png create mode 100644 aur/icons/32x32.png create mode 100644 aur/icons/48x48.png create mode 100644 aur/icons/512x512.png create mode 100644 aur/icons/64x64.png create mode 100644 aur/icons/8x8.png create mode 100644 aur/icons/96x96.png diff --git a/aur/bauh.svg b/aur/bauh.svg deleted file mode 100644 index 2714f294..00000000 --- a/aur/bauh.svg +++ /dev/null @@ -1,477 +0,0 @@ - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Layer 1 \ No newline at end of file diff --git a/aur/icons/128x128.png b/aur/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..53088ba37d8e7612d7f667f0140e24eaa2f9fbd5 GIT binary patch literal 6580 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 P00000NkvXXu0mjfn@pN@ literal 0 HcmV?d00001 diff --git a/aur/icons/16x16.png b/aur/icons/16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b476ca5c7c9b0848ab594ac30b8524e5f47e24 GIT binary patch literal 1011 zcmVEX>4Tx04R}tkv&MmKpe$iQ>9WW7R(^xkfAzRC@SKpRVYG*P%E_RU_SZkM+H^bh|{W*Vj)BONgw~P>leu-ldA%S z91EyHgXH?b{@{1FR&jE|ONu0c-WSLD7zIMRK&#<6-^Y&AIst;uz?I(d*P6i0C+Urj z7CQn4wtQ&$$tyU3izn^q&_yKjPQJJ!gb;B>C*~H^a!*!t zAI=}QaY)-Al~PVCVN0Boh@HyQV&iW_Bhr2NeuwJV2Zh4EvT#1%qrQnEgd8SFZA$a=W6sGt0 z;m0ZgpOc{9iQR128)93~X1!G9?eZo|xgDg&$ZBJo=4O@C-ouOz4|Iy1U-(tfrdX_c9swMu6#TL1=*6 hFEZKW!})wq_9r`2v%dj`XYv35002ovPDHLkV1h^6(uV*5 literal 0 HcmV?d00001 diff --git a/aur/icons/24x24.png b/aur/icons/24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..2f4eb52559dd50065a526546dab8dfea787d04b3 GIT binary patch literal 1433 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 literal 0 HcmV?d00001 diff --git a/aur/icons/32x32.png b/aur/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..c84ecae905a67cc278b168ac4a2b745615d7c350 GIT binary patch literal 1667 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 literal 0 HcmV?d00001 diff --git a/aur/icons/512x512.png b/aur/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..38b946cdb761a4dae8ac605e04e5fb301855d749 GIT binary patch literal 22325 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 literal 0 HcmV?d00001 diff --git a/aur/icons/8x8.png b/aur/icons/8x8.png new file mode 100644 index 0000000000000000000000000000000000000000..e73905c56d6a881ddbf2ddc6a85a1d277621b3be GIT binary patch literal 726 zcmV;{0xA88P)EX>4Tx04R}tkv&MmKpe$iQ>9WW7R(^xkfAzRC@SKpRVYG*P%E_RU_SZkM+H^bh|{W*Vj)BONgw~P>leu-ldA%S z91EyHgXH?b{@{1FR&jE|ONu0c-WSLD7zIMRK&#<6-^Y&AIst;uz?I(d*P6i0C+Urj z7CQn4wt~4_{lmY1|2_En=RX5upthvLe+GsI78?nBAukPvTYKdgMA^46+*$bG z5Ca2419M-z{{7$o{wh3q%E`bYp~mp}7eB*?E7usb*xBWMO;q9;&on#zXJBApc=hoI z!-<>k86Lm=#bBZ!&LGUo$-uzCz`())1!DXh41Q*k40oO|G9Uvc1_FSIfq}u1fq~%w z7P*5A3=B?;3=9kmCUV>iGg8eR7#J8DpjI??Y(BC7!n3ao0P6ES9k41`KL7v#07*qo IM6N<$f_daPfdBvi literal 0 HcmV?d00001 diff --git a/aur/icons/96x96.png b/aur/icons/96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..9de5638d54bb973ba4560e9f5337de018af38472 GIT binary patch literal 4429 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 Date: Sun, 20 Oct 2019 14:41:55 -0300 Subject: [PATCH 04/46] aur dist -> 'copy_icons' script --- aur/copy_icons.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 aur/copy_icons.py diff --git a/aur/copy_icons.py b/aur/copy_icons.py new file mode 100644 index 00000000..da13149e --- /dev/null +++ b/aur/copy_icons.py @@ -0,0 +1,14 @@ +import glob +import shutil +import sys +from pathlib import Path + +output_base = sys.argv[2] + +for f in glob.glob(sys.argv[1] + '/*.png'): + res = f.split('/')[-1].split('.') + dest_dir = output_base + '/' + res + '/apps' + + Path(dest_dir).mkdir(parents=True, exist_ok=True) + + shutil.copy(f, dest_dir + '/bauh.png') From 20517ad4793dd7046abb94cfacf92e067aed522f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 14:45:59 -0300 Subject: [PATCH 05/46] aur dist -> fix 'copy_icons' script --- aur/copy_icons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aur/copy_icons.py b/aur/copy_icons.py index da13149e..6c684f71 100644 --- a/aur/copy_icons.py +++ b/aur/copy_icons.py @@ -6,7 +6,7 @@ from pathlib import Path output_base = sys.argv[2] for f in glob.glob(sys.argv[1] + '/*.png'): - res = f.split('/')[-1].split('.') + res = f.split('/')[-1].split('.')[0] dest_dir = output_base + '/' + res + '/apps' Path(dest_dir).mkdir(parents=True, exist_ok=True) From fcc12721aae3ad58b955590029fc800ffe450dc6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 14:50:25 -0300 Subject: [PATCH 06/46] aur dist -> 'bauh-tray' script --- aur/bauh-tray | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 aur/bauh-tray diff --git a/aur/bauh-tray b/aur/bauh-tray new file mode 100644 index 00000000..c42e0db0 --- /dev/null +++ b/aur/bauh-tray @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/bauh --tray=1 From 1245062b11cfdf6522adb9467ed04a192abff72d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 20 Oct 2019 14:56:10 -0300 Subject: [PATCH 07/46] aur dist -> fix bauh_tray.desktop --- aur/bauh_tray.desktop | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aur/bauh_tray.desktop b/aur/bauh_tray.desktop index 9b4f325f..3b7abd19 100644 --- a/aur/bauh_tray.desktop +++ b/aur/bauh_tray.desktop @@ -1,6 +1,8 @@ [Desktop Entry] Type=Application -Name=bauh +Name=bauh ( tray ) +Name[pt]=bauh ( bandeja ) +Name[es]=bauh ( bandeja ) Categories=System; Comment=Manage your Flatpak / Snap / AppImage / AUR applications Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR From 8ea182da2a24190b99afc0cf33aea4fd476612a0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 12:11:06 -0300 Subject: [PATCH 08/46] [aur] fix -> update-checking for some scenarios described in Manjaro's forum --- CHANGELOG.md | 5 +++++ bauh/__init__.py | 2 +- bauh/gems/arch/mapper.py | 28 ++++++++++++++---------- tests/gems/arch/arch_data_mapper_test.py | 3 +++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee298e32..999dbc0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.7.1] 2019- +### Fixes +- AUR: + - update-checking for some scenarios + ## [0.7.0] 2019-10-18 ### Features - AppImage support ( see below ) diff --git a/bauh/__init__.py b/bauh/__init__.py index e1ab2566..3fdb7b7e 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.7.0' +__version__ = '0.7.1' __app_name__ = 'bauh' import os diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index ffee5a99..d058d3c3 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -7,8 +7,7 @@ from bauh.gems.arch.model import ArchPackage URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}' RE_LETTERS = re.compile(r'\.([a-zA-Z]+)-\d+$') -RE_ANY_LETTER = re.compile(r'[a-zA-Z]') -RE_VERSION_DELS = re.compile(r'[.:#@\-_]') +RE_VERSION_SPLIT = re.compile(r'[a-zA-Z]+|\d+|[\.\-_@#]+') RE_SFX = ('r', 're', 'release') GA_SFX = ('ga', 'ge') @@ -76,19 +75,18 @@ class ArchDataMapper: return nlatest > nversion - latest_split = RE_VERSION_DELS.split(latest_version) - version_split = RE_VERSION_DELS.split(version) + latest_split = RE_VERSION_SPLIT.findall(latest_version) + current_split = RE_VERSION_SPLIT.findall(version) for idx in range(len(latest_split)): - if idx < len(version_split): + if idx < len(current_split): latest_part = latest_split[idx] - version_part = version_split[idx] + current_part = current_split[idx] - if latest_part != version_part: - if RE_ANY_LETTER.findall(latest_part) or RE_ANY_LETTER.findall(version_part): - return latest_part > version_part - else: - dif = int(latest_part) - int(version_part) + if latest_part != current_part: + + try: + dif = int(latest_part) - int(current_part) if dif > 0: return True @@ -96,6 +94,14 @@ class ArchDataMapper: return False else: continue + + except ValueError: + if latest_part.isdigit(): + return True + elif current_part.isdigit(): + return False + else: + return latest_part > current_part return False def fill_package_build(self, pkg: ArchPackage): diff --git a/tests/gems/arch/arch_data_mapper_test.py b/tests/gems/arch/arch_data_mapper_test.py index 3aaf4a4a..6dfb42c8 100644 --- a/tests/gems/arch/arch_data_mapper_test.py +++ b/tests/gems/arch/arch_data_mapper_test.py @@ -24,6 +24,9 @@ class ArchDataMapperTest(TestCase): self.assertFalse(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.90-1')) self.assertTrue(ArchDataMapper.check_update('77.0.3865.a-1', '77.0.3865.b-1')) self.assertFalse(ArchDataMapper.check_update('77.0.b.0-1', '77.0.a.1-1')) + self.assertFalse(ArchDataMapper.check_update('r25.e22697c-1', 'r8.19fe011-1')) + self.assertFalse(ArchDataMapper.check_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1')) + self.assertFalse(ArchDataMapper.check_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3')) def test_check_update_no_suffix_3_x_2_digits(self): self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.1-1')) From 42a3be984c557cc19538988a7e9805e50005983f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 12:55:05 -0300 Subject: [PATCH 09/46] [aur] fix -> not respecting 'ignorepkg' settings in pacman.conf --- CHANGELOG.md | 3 +- bauh/gems/arch/pacman.py | 37 +++++++ tests/gems/arch/pacman_test.py | 23 ++++ tests/gems/arch/resources/pacman.conf | 98 +++++++++++++++++ .../gems/arch/resources/pacman_ign_pkgs.conf | 101 ++++++++++++++++++ 5 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 tests/gems/arch/pacman_test.py create mode 100644 tests/gems/arch/resources/pacman.conf create mode 100644 tests/gems/arch/resources/pacman_ign_pkgs.conf diff --git a/CHANGELOG.md b/CHANGELOG.md index 999dbc0d..5f7a7ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - AUR: - update-checking for some scenarios + - not respecting 'ignorepkg' settings in **pacman.conf** ## [0.7.0] 2019-10-18 ### Features @@ -39,7 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### AppImage support - Search, install, uninstall, downgrade, launch and retrieve the applications history - Supported sources: [AppImageHub](https://appimage.github.io) ( **applications with no releases published to GitHub are currently not available** ) -- Adds desktop entries ( menu shortcuts ) for the installed applications ( **~/.local/share/applications **) +- Adds desktop entries ( menu shortcuts ) for the installed applications ( **~/.local/share/applications**) ## [0.6.4] 2019-10-13 ### Fixes diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 7681e2b2..678355f0 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -1,4 +1,5 @@ import re +from threading import Thread from typing import List, Set from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess @@ -71,10 +72,18 @@ def check_installed(pkg: str) -> bool: return bool(res) +def _fill_ignored(res: dict): + res['pkgs'] = list_ignored_packages() + + def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info + ignored = {} + thread_ignored = Thread(target=_fill_ignored, args=(ignored,)) + thread_ignored.start() + pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {} for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], stdin=allinfo).stdout: # filtering only the Name and Validated By fields: @@ -96,6 +105,18 @@ def list_and_map_installed() -> dict: # returns a dict with with package names current_pkg = {} + if pkgs and pkgs.get('not_signed'): + thread_ignored.join() + + if ignored['pkgs']: + to_del = set() + for pkg in pkgs['not_signed'].keys(): + if pkg in ignored['pkgs']: + to_del.add(pkg) + + for pkg in to_del: + del pkgs['not_signed'][pkg] + return pkgs @@ -179,3 +200,19 @@ def receive_key(key: str, root_password: str) -> SystemProcess: def sign_key(key: str, root_password: str) -> SystemProcess: return SystemProcess(new_root_subprocess(['pacman-key', '--lsign-key', key], root_password=root_password), check_error_output=False) + +def list_ignored_packages(config_path: str = '/etc/pacman.conf') -> Set[str]: + pacman_conf = new_subprocess(['cat', config_path]) + + ignored = set() + grep = new_subprocess(['grep', '-Eo', r'\s*#*\s*ignorepkg\s*=\s*.+'], stdin=pacman_conf.stdout) + for o in grep.stdout: + if o: + line = o.decode().strip() + + if not line.startswith('#'): + ignored.add(line.split('=')[1].strip()) + + pacman_conf.terminate() + grep.terminate() + return ignored diff --git a/tests/gems/arch/pacman_test.py b/tests/gems/arch/pacman_test.py new file mode 100644 index 00000000..ea7e2c25 --- /dev/null +++ b/tests/gems/arch/pacman_test.py @@ -0,0 +1,23 @@ +import os +from unittest import TestCase + +from bauh.gems.arch import pacman + +FILE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class PacmanTest(TestCase): + + def test_list_ignored_packages(self): + ignored = pacman.list_ignored_packages(FILE_DIR + '/resources/pacman_ign_pkgs.conf') + + self.assertIsNotNone(ignored) + self.assertEqual(2, len(ignored)) + self.assertIn('google-chrome', ignored) + self.assertIn('firefox', ignored) + + def test_list_ignored_packages__no_ignored_packages(self): + ignored = pacman.list_ignored_packages(FILE_DIR + '/resources/pacman.conf') + + self.assertIsNotNone(ignored) + self.assertEqual(0, len(ignored)) diff --git a/tests/gems/arch/resources/pacman.conf b/tests/gems/arch/resources/pacman.conf new file mode 100644 index 00000000..0649ebfc --- /dev/null +++ b/tests/gems/arch/resources/pacman.conf @@ -0,0 +1,98 @@ +# +# /etc/pacman.conf +# +# See the pacman.conf(5) manpage for option and repository directives + +# +# GENERAL OPTIONS +# +[options] +# The following paths are commented out with their default values listed. +# If you wish to use different paths, uncomment and update the paths. +#RootDir = / +#DBPath = /var/lib/pacman/ +CacheDir = /var/cache/pacman/pkg/ +#LogFile = /var/log/pacman.log +#GPGDir = /etc/pacman.d/gnupg/ +#HookDir = /etc/pacman.d/hooks/ +HoldPkg = pacman glibc manjaro-system +# If upgrades are available for these packages they will be asked for first +SyncFirst = manjaro-system archlinux-keyring manjaro-keyring +#XferCommand = /usr/bin/curl -C - -f %u > %o +#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u +#CleanMethod = KeepInstalled +#UseDelta = 0.7 +Architecture = auto + +# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup +#IgnorePkg = +#IgnoreGroup = + +#NoUpgrade = +#NoExtract = + +# Misc options +#UseSyslog +#Color +#TotalDownload +# We cannot check disk space from within a chroot environment +CheckSpace +#VerbosePkgLists + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +#RemoteFileSigLevel = Required + +# NOTE: You must run `pacman-key --init` before first using pacman; the local +# keyring can then be populated with the keys of all official Manjaro Linux +# packagers with `pacman-key --populate archlinux manjaro`. + +# +# REPOSITORIES +# - can be defined here or included from another file +# - pacman will search repositories in the order defined here +# - local/custom mirrors can be added here or in separate files +# - repositories listed first will take precedence when packages +# have identical names, regardless of version number +# - URLs will have $repo replaced by the name of the current repo +# - URLs will have $arch replaced by the name of the architecture +# +# Repository entries are of the format: +# [repo-name] +# Server = ServerName +# Include = IncludePath +# +# The header [repo-name] is crucial - it must be present and +# uncommented to enable the repo. +# + +# The testing repositories are disabled by default. To enable, uncomment the +# repo name header and Include lines. You can add preferred servers immediately +# after the header, and they will be used before the default mirrors. + +[core] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +[extra] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +[community] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +# If you want to run 32 bit applications on your x86_64 system, +# enable the multilib repositories as required here. + +[multilib] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +# An example of a custom package repository. See the pacman manpage for +# tips on creating your own repositories. +#[custom] +#SigLevel = Optional TrustAll +#Server = file:///home/custompkgs \ No newline at end of file diff --git a/tests/gems/arch/resources/pacman_ign_pkgs.conf b/tests/gems/arch/resources/pacman_ign_pkgs.conf new file mode 100644 index 00000000..6e502ab0 --- /dev/null +++ b/tests/gems/arch/resources/pacman_ign_pkgs.conf @@ -0,0 +1,101 @@ +# +# /etc/pacman.conf +# +# See the pacman.conf(5) manpage for option and repository directives + +# +# GENERAL OPTIONS +# +[options] +# The following paths are commented out with their default values listed. +# If you wish to use different paths, uncomment and update the paths. +#RootDir = / +#DBPath = /var/lib/pacman/ +CacheDir = /var/cache/pacman/pkg/ +#LogFile = /var/log/pacman.log +#GPGDir = /etc/pacman.d/gnupg/ +#HookDir = /etc/pacman.d/hooks/ +HoldPkg = pacman glibc manjaro-system +# If upgrades are available for these packages they will be asked for first +SyncFirst = manjaro-system archlinux-keyring manjaro-keyring +#XferCommand = /usr/bin/curl -C - -f %u > %o +#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u +#CleanMethod = KeepInstalled +#UseDelta = 0.7 +Architecture = auto + +# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup +#IgnorePkg = +#IgnoreGroup = + +#NoUpgrade = +#NoExtract = + +# Misc options +#UseSyslog +#Color +#TotalDownload +# We cannot check disk space from within a chroot environment +CheckSpace +#VerbosePkgLists + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +#RemoteFileSigLevel = Required + +# NOTE: You must run `pacman-key --init` before first using pacman; the local +# keyring can then be populated with the keys of all official Manjaro Linux +# packagers with `pacman-key --populate archlinux manjaro`. + +# +# REPOSITORIES +# - can be defined here or included from another file +# - pacman will search repositories in the order defined here +# - local/custom mirrors can be added here or in separate files +# - repositories listed first will take precedence when packages +# have identical names, regardless of version number +# - URLs will have $repo replaced by the name of the current repo +# - URLs will have $arch replaced by the name of the architecture +# +# Repository entries are of the format: +# [repo-name] +# Server = ServerName +# Include = IncludePath +# +# The header [repo-name] is crucial - it must be present and +# uncommented to enable the repo. +# + +# The testing repositories are disabled by default. To enable, uncomment the +# repo name header and Include lines. You can add preferred servers immediately +# after the header, and they will be used before the default mirrors. + +[core] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +[extra] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +[community] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +# If you want to run 32 bit applications on your x86_64 system, +# enable the multilib repositories as required here. + +[multilib] +SigLevel = PackageRequired +Include = /etc/pacman.d/mirrorlist + +# An example of a custom package repository. See the pacman manpage for +# tips on creating your own repositories. +#[custom] +#SigLevel = Optional TrustAll +#Server = file:///home/custompkgs +ignorepkg=google-chrome +# ignorepkg=abc +ignorepkg = firefox \ No newline at end of file From b0d09312c577b5b74a8cb5e4cc3ef196f71c94a5 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 14:55:13 -0300 Subject: [PATCH 10/46] [aur] fix: not able to handle missing dependencies with symbols --- CHANGELOG.md | 1 + bauh/gems/arch/pacman.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7a7ee3..0a972ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AUR: - update-checking for some scenarios - not respecting 'ignorepkg' settings in **pacman.conf** + - not able to handle **missing dependencies with symbols** ( e.g: libpng++ ) ## [0.7.0] 2019-10-18 ### Features diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 678355f0..91a9a279 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -14,7 +14,7 @@ def is_enabled() -> bool: def get_mirrors(pkgs: Set[str]) -> dict: - pkgre = '|'.join(pkgs) + pkgre = '|'.join(pkgs).replace('+', r'\+').replace('.', r'\.') searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout mirrors = {} From 7d9104d139fb81afab47cad4b58a93f675d67c90 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 15:21:52 -0300 Subject: [PATCH 11/46] [aur] fix: not able to work with .xpm icons --- CHANGELOG.md | 1 + bauh/gems/arch/disk.py | 3 +-- bauh/gems/arch/pacman.py | 2 +- bauh/gems/arch/worker.py | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a972ca8..8617f1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - update-checking for some scenarios - not respecting 'ignorepkg' settings in **pacman.conf** - not able to handle **missing dependencies with symbols** ( e.g: libpng++ ) + - not able to work with **.xpm** icons ## [0.7.0] 2019-10-18 ### Features diff --git a/bauh/gems/arch/disk.py b/bauh/gems/arch/disk.py index 507288e9..9e012f05 100644 --- a/bauh/gems/arch/disk.py +++ b/bauh/gems/arch/disk.py @@ -22,7 +22,7 @@ def write(app: ArchPackage): def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool): - ends_with = re.compile(r'.+/{}\.(png|svg)$'.format(app.icon_path if app.icon_path else app.name), re.IGNORECASE) + ends_with = re.compile(r'.+/{}\.(png|svg|xpm)$'.format(app.icon_path if app.icon_path else app.name), re.IGNORECASE) for path in icon_paths: if ends_with.match(path): @@ -142,4 +142,3 @@ def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, mainta write(p) return len(to_write) return 0 - diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 91a9a279..9e8e8d4d 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -145,7 +145,7 @@ def list_icon_paths(pkgnames: Set[str]) -> List[str]: installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]) icon_files = [] - for out in new_subprocess(['grep', '-E', '.(png|svg)$'], stdin=installed_files.stdout).stdout: + for out in new_subprocess(['grep', '-E', '.(png|svg|xpm)$'], stdin=installed_files.stdout).stdout: if out: line = out.decode().strip() if line: diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 50910c9c..978ba102 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -8,8 +8,6 @@ from multiprocessing import Process from threading import Thread from typing import Dict, List -import requests - from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager from bauh.api.constants import HOME_PATH From 887c62f878bccb2172c7f4fbe2485ca0d2d461f8 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 18:03:31 -0300 Subject: [PATCH 12/46] [setup.py] --- CHANGELOG.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8617f1fc..bf10061d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - AUR: - update-checking for some scenarios - - not respecting 'ignorepkg' settings in **pacman.conf** + - not respecting **ignorepkg** settings in **pacman.conf** - not able to handle **missing dependencies with symbols** ( e.g: libpng++ ) - not able to work with **.xpm** icons diff --git a/setup.py b/setup.py index a0de0e07..d9ac265b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import os from setuptools import setup, find_packages DESCRIPTION = ( - "Graphical user interface to manage Flatpak / Snap applications." + "Graphical interface to manage Flatpak, Snap, AppImage and AUR packages" ) AUTHOR = "Vinicius Moreira" From d80cac19af96a809e8da5c44b5529f96bd74bf2a Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 18:48:48 -0300 Subject: [PATCH 13/46] [aur] showing a popup when there are integrity issues with the source-files of a building package --- CHANGELOG.md | 3 +++ bauh/gems/arch/controller.py | 6 ++++++ bauh/gems/arch/makepkg.py | 3 +++ bauh/gems/arch/resources/locale/en | 4 +++- bauh/gems/arch/resources/locale/es | 4 +++- bauh/gems/arch/resources/locale/pt | 4 +++- 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf10061d..2a4e5387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.7.1] 2019- +### Improvements +- AUR: + - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package ### Fixes - AUR: - update-checking for some scenarios diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index a62b97a2..358e1a59 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -490,6 +490,12 @@ class ArchManager(SoftwareManager): handler.watcher.print(self.i18n['action.cancelled']) return False + if check_res.get('validity_check'): + handler.watcher.show_message(title=self.i18n['arch.aur.install.validity_check.title'], + body=self.i18n['arch.aur.install.validity_check.body'].format(bold(pkgname)), + type_=MessageType.ERROR) + return False + return True def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool: diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index 2255dd12..3a72b002 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -18,4 +18,7 @@ def check(pkgdir: str, handler: ProcessHandler) -> dict: if gpg_keys: res['gpg_key'] = gpg_keys[0] + if 'One or more files did not pass the validity check' in output: + res['validity_check'] = True + return res diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index bb4deba5..47e1cc3b 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -60,4 +60,6 @@ arch.aur.install.pgp.receive_fail=Could not receive PGP key {} arch.aur.install.unknown_key.title=Public key required arch.install.aur.unknown_key.body=To continue {} installation is necessary to trust the following public key {} arch.aur.install.unknown_key.status=Receiving public key {} -arch.aur.install.unknown_key.receive_error=Could not receive public key {} \ No newline at end of file +arch.aur.install.unknown_key.receive_error=Could not receive public key {} +arch.aur.install.validity_check.title=Integrity issues +arch.aur.install.validity_check.body=Some of the source-files needed for {} installation are not healthy. The installation will be cancelled to avoid damages to your system. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index d97b21b0..d8a54105 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -92,4 +92,6 @@ arch.aur.install.pgp.sign_fail=No fue posible recibir la clave PGP {} arch.aur.install.unknown_key.title=Clave pública necesaria arch.aur.install.unknown_key.status=Recibiendo la clave pública {} arch.aur.install.unknown_key.receive_error=No fue posible recibir la clave pública {} -arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {} \ No newline at end of file +arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {} +arch.aur.install.validity_check.title=Problemas de integridad +arch.aur.install.validity_check.body=Algunos de los archivos fuente necesarios para la instalación de {} no están en buen estado. La instalación se cancelará para evitar daños a su sistema. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 5b521483..a5d82578 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -92,4 +92,6 @@ arch.aur.install.pgp.receive_fail=Não foi possível receber a chave PGP {} arch.aur.install.unknown_key.title=Chave pública necessária arch.install.aur.unknown_key.body=Para continuar a instalação de {} é ncessário confiar na seguinte chave pública {} arch.aur.install.unknown_key.status=Recebendo a chave pública {} -arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {} \ No newline at end of file +arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {} +arch.aur.install.validity_check.title=Problemas de integridade +arch.aur.install.validity_check.body=Alguns dos arquivos-fonte necessários para instalação de {} não estão íntegros. A instalação será cancelada para evitar danos no seu sistema. \ No newline at end of file From a33ede646aa016d177b6904325379a23c248e182 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 21 Oct 2019 19:13:14 -0300 Subject: [PATCH 14/46] [root dialog] 3 password attempts for root authentication --- CHANGELOG.md | 1 + bauh/view/qt/root.py | 37 +++++++++++++++++++++++------------ bauh/view/resources/locale/en | 1 + bauh/view/resources/locale/es | 1 + bauh/view/resources/locale/pt | 1 + 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a4e5387..53445937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.7.1] 2019- ### Improvements +- 3 password attempts for root authentication - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package ### Fixes diff --git a/bauh/view/qt/root.py b/bauh/view/qt/root.py index 890c487f..d4f16cb0 100644 --- a/bauh/view/qt/root.py +++ b/bauh/view/qt/root.py @@ -13,28 +13,41 @@ def is_root(): return os.getuid() == 0 -def ask_root_password(locale_keys: dict): - +def ask_root_password(i18n: dict): diag = QInputDialog() diag.setStyleSheet("""QLineEdit { border-radius: 5px; font-size: 16px }""") diag.setInputMode(QInputDialog.TextInput) diag.setTextEchoMode(QLineEdit.Password) diag.setWindowIcon(QIcon(resource.get_path('img/lock.png'))) - diag.setWindowTitle(locale_keys['popup.root.title']) + diag.setWindowTitle(i18n['popup.root.title']) diag.setLabelText('') - diag.setCancelButtonText(locale_keys['popup.button.cancel']) + diag.setCancelButtonText(i18n['popup.button.cancel']) diag.resize(400, 200) - ok = diag.exec_() + for attempt in range(3): - if ok: - if not validate_password(diag.textValue()): - show_message(title=locale_keys['popup.root.bad_password.title'], - body=locale_keys['popup.root.bad_password.body'], - type_=MessageType.ERROR) - ok = False + ok = diag.exec_() - return diag.textValue(), ok + if ok: + if not validate_password(diag.textValue()): + + body = i18n['popup.root.bad_password.body'] + + if attempt == 2: + body += '. ' + i18n['popup.root.bad_password.last_try'] + + show_message(title=i18n['popup.root.bad_password.title'], + body=body, + type_=MessageType.ERROR) + ok = False + diag.setTextValue('') + + if ok: + return diag.textValue(), ok + else: + break + + return '', False def validate_password(password: str) -> bool: diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 029a4b4c..04523f1a 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -38,6 +38,7 @@ manage_window.checkbox.show_details=Show details popup.root.title=Requires your password to continue popup.root.bad_password.title=Error popup.root.bad_password.body=Wrong password +popup.root.bad_password.last_try=Attempts finished. Action cancelled. popup.history.title=History popup.history.selected.tooltip=Current version popup.button.yes=Yes diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index dfb29385..b9e055b8 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -39,6 +39,7 @@ manage_window.checkbox.show_details=Mostrar detalles popup.root.title=Requiere tu contraseña para continuar popup.root.bad_password.title=Error popup.root.bad_password.body=Contraseña incorrecta +popup.root.bad_password.last_try=Intentos finalizados. Acción cancelada. popup.history.title=Historia popup.history.selected.tooltip=Versión actual popup.button.yes=Sí diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 728ad2f2..53a7a50d 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -39,6 +39,7 @@ manage_window.checkbox.show_details=Mostrar detalhes popup.root.title=Requer sua senha para prosseguir popup.root.bad_password.title=Erro popup.root.bad_password.body=Senha incorreta +popup.root.bad_password.last_try=Tentativas finalizadas. Ação cancelada. popup.history.title=Histórico popup.history.selected.tooltip=Versão atual popup.button.yes=Sim From 696ce1c79ebe48000bd39f14d0e78f325d88e2ff Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 10:18:35 -0300 Subject: [PATCH 15/46] fix: application is not initializing when there is no internet connection --- CHANGELOG.md | 2 ++ bauh/gems/appimage/worker.py | 8 +++++++- bauh/gems/arch/worker.py | 39 +++++++++++++++++++++--------------- bauh/gems/snap/worker.py | 38 ++++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53445937..c7113837 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 3 password attempts for root authentication - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package + ### Fixes +- application not initializing when there is no internet connection - AUR: - update-checking for some scenarios - not respecting **ignorepkg** settings in **pacman.conf** diff --git a/bauh/gems/appimage/worker.py b/bauh/gems/appimage/worker.py index fcc6929e..e9449cbe 100644 --- a/bauh/gems/appimage/worker.py +++ b/bauh/gems/appimage/worker.py @@ -8,6 +8,8 @@ from multiprocessing import Process from pathlib import Path from threading import Thread +import requests + from bauh.api.http import HttpClient from bauh.gems.appimage import LOCAL_PATH, db @@ -68,7 +70,11 @@ class DatabaseUpdater(Thread if bool(int(os.getenv('BAUH_DEBUG', 0))) else Proce def run(self): if self.enabled: while True: - self._download_databases() + try: + self._download_databases() + except requests.exceptions.ConnectionError: + self.logger.warning('The internet connection seems to be off.') + self.logger.info('Sleeping') time.sleep(self.sleep) else: diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 978ba102..a0f69558 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -8,6 +8,8 @@ from multiprocessing import Process from threading import Thread from typing import Dict, List +import requests + from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager from bauh.api.constants import HOME_PATH @@ -45,7 +47,7 @@ class AURIndexUpdater(Thread): self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index))) else: self.logger.warning('No data returned from: {}'.format(URL_INDEX)) - except ConnectionError: + except requests.exceptions.ConnectionError: self.logger.warning('No internet connection: could not pre-index packages') time.sleep(60 * 20) # updates every 20 minutes @@ -148,21 +150,26 @@ class CategoriesDownloader: def get_categories(self) -> Dict[str, List[str]]: self.logger.info('Downloading AUR category definitions from {}'.format(self.URL_CATEGORIES_FILE)) - res = self.http_client.get(self.URL_CATEGORIES_FILE) + try: + res = self.http_client.get(self.URL_CATEGORIES_FILE) - if res: - try: - categories_map = {} - for l in res.text.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + if res: + try: + categories_map = {} + for l in res.text.split('\n'): + if l: + data = l.split('=') + categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] - self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map))) - return categories_map - except: - self.logger.error("Could not parse AUR categories definitions") - traceback.print_exc() + self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map))) + return categories_map + except: + self.logger.error("Could not parse AUR categories definitions") + traceback.print_exc() + return {} + else: + self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) return {} - else: - self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) + except requests.exceptions.ConnectionError: + self.logger.warning('The internet connection seems to be off.') + return {} diff --git a/bauh/gems/snap/worker.py b/bauh/gems/snap/worker.py index 6bfd9c08..54bd1271 100644 --- a/bauh/gems/snap/worker.py +++ b/bauh/gems/snap/worker.py @@ -4,6 +4,8 @@ import traceback from threading import Thread from typing import Dict, List +import requests + from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager @@ -85,21 +87,25 @@ class CategoriesDownloader: def get_categories(self) -> Dict[str, List[str]]: self.logger.info('Downloading Snap category definitions from {}'.format(self.URL_CATEGORIES_FILE)) - res = self.http_client.get(self.URL_CATEGORIES_FILE) + try: + res = self.http_client.get(self.URL_CATEGORIES_FILE) - if res: - try: - categories_map = {} - for l in res.text.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + if res: + try: + categories_map = {} + for l in res.text.split('\n'): + if l: + data = l.split('=') + categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] - self.logger.info('Loaded categories for {} Snap applications'.format(len(categories_map))) - return categories_map - except: - self.logger.error("Could not parse categories definitions") - traceback.print_exc() - return {} - else: - self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) + self.logger.info('Loaded categories for {} Snap applications'.format(len(categories_map))) + return categories_map + except: + self.logger.error("Could not parse categories definitions") + traceback.print_exc() + return {} + else: + self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) + except requests.exceptions.ConnectionError: + self.logger.warning('The internet connection seems to be off.') + return {} From 98367219dc34c9ec9443c3eb48419756554f0d99 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 11:53:56 -0300 Subject: [PATCH 16/46] [snap] feature -> allowing the user to install an application when a stable channel is not available --- CHANGELOG.md | 4 ++++ bauh/api/abstract/view.py | 3 --- bauh/commons/system.py | 5 ++++- bauh/gems/snap/controller.py | 31 +++++++++++++++++++++++++++--- bauh/gems/snap/resources/locale/en | 4 +++- bauh/gems/snap/resources/locale/es | 4 +++- bauh/gems/snap/resources/locale/pt | 4 +++- bauh/gems/snap/snap.py | 7 ++++--- bauh/view/qt/components.py | 2 +- bauh/view/resources/locale/en | 5 ++++- bauh/view/resources/locale/es | 5 ++++- bauh/view/resources/locale/pt | 5 ++++- 12 files changed, 62 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7113837..8bcd2e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.7.1] 2019- +### Features +- Snap: + - if the **stable** channel is not available while an application is being installed, a popup is displayed allowing the user to choose a different one ( e.g: dbeaver-ce ) + ### Improvements - 3 password attempts for root authentication - AUR: diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 64136a4f..3caf1dc1 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -59,9 +59,6 @@ class SingleSelectComponent(InputViewComponent): def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, max_per_line: int = 1, id_: str = None): super(SingleSelectComponent, self).__init__(id_=id_) - if options is None or len(options) < 2: - raise Exception("'options' must be a list with at least 2 elements") - self.type = type_ self.label = label self.options = options diff --git a/bauh/commons/system.py b/bauh/commons/system.py index ab37903f..6a555fbb 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -100,7 +100,7 @@ class ProcessHandler: if self.watcher: self.watcher.print(msg) - def handle(self, process: SystemProcess) -> bool: + def handle(self, process: SystemProcess, error_output: StringIO = None) -> bool: self._notify_watcher(' '.join(process.subproc.args) + '\n') already_succeeded = False @@ -126,6 +126,9 @@ class ProcessHandler: if line: self._notify_watcher(line) + if error_output is not None: + error_output.write(line) + if process.check_error_output: if process.wrong_error_phrase and process.wrong_error_phrase in line: continue diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 220b90e3..0ab35e23 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -1,3 +1,4 @@ +import re import time from datetime import datetime from threading import Thread @@ -8,13 +9,15 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority +from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption from bauh.commons.html import bold -from bauh.commons.system import SystemProcess, ProcessHandler +from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess from bauh.gems.snap import snap, suggestions from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.model import SnapApplication from bauh.gems.snap.worker import SnapAsyncDataLoader, CategoriesDownloader +RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)')) class SnapManager(SoftwareManager): @@ -125,9 +128,31 @@ class SnapManager(SoftwareManager): raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__)) def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: - res = ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password))) + res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password)) - if res: + if 'error:' in output: + res = False + if 'not available on stable' in output: + channels = RE_AVAILABLE_CHANNELS.findall(output) + + if channels: + opts = [InputOption(label=c[0], value=c[1]) for c in channels] + channel_select = SingleSelectComponent(type_=SelectViewType.RADIO, label='', options=opts, default_option=opts[0]) + if watcher.request_confirmation(title=self.i18n['snap.install.available_channels.title'], + body=self.i18n['snap.install.available_channels.body'].format(bold(self.i18n['stable']), bold(pkg.name)) + ':', + components=[channel_select], + confirmation_label=self.i18n['continue'], + deny_label=self.i18n['cancel']): + self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value)) + res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password))) + + if res: + pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro) + + return res + else: + self.logger.error("Could not find available channels in the installation output: {}".format(output)) + else: pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro) return res diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 3426cdc3..633cd875 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -2,4 +2,6 @@ gem.snap.info=Applications published at https://snapcraft.io/store snap.notification.snapd_unavailable={} seems not to be installed or enabled. {} packages will not be available. snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {} snap.action.refresh.status=Refreshing -snap.action.refresh.label=Refresh \ No newline at end of file +snap.action.refresh.label=Refresh +snap.install.available_channels.title=Available channels +snap.install.available_channels.body=There is no {} channel available for {}. But there are these others available below ( select one if you want to continue ) \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index 0d2f1a11..0989e73b 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -13,4 +13,6 @@ snap.info.size=tamaño snap.notification.snapd_unavailable={} no parece estar instalado o habilitado. Los paquetes {} estarán indisponibles. snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} snap.action.refresh.status=Actualizando -snap.action.refresh.label=Actualizar \ No newline at end of file +snap.action.refresh.label=Actualizar +snap.install.available_channels.title=Canales disponibles +snap.install.available_channels.body=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros disponibles abajo ( seleccione uno si desea continuar ) \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 0828ae2d..e93ae589 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -13,4 +13,6 @@ snap.info.size=tamanho snap.notification.snapd_unavailable={} não parece estar instalado ou habilitado. Os pacotes {} estarão indisponíveis. snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {} snap.action.refresh.status=Atualizando -snap.action.refresh.label=Atualizar \ No newline at end of file +snap.action.refresh.label=Atualizar +snap.install.available_channels.title=Canais disponíveis +snap.install.available_channels.body=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros disponíveis abaixo ( selecione algum se quiser continuar ) \ No newline at end of file diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 77384628..b2c252cc 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -3,7 +3,7 @@ import re import subprocess from typing import List -from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess +from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess, SimpleProcess from bauh.gems.snap.model import SnapApplication BASE_CMD = 'snap' @@ -176,14 +176,15 @@ def uninstall_and_stream(app_name: str, root_password: str): return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password) -def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen: +def install_and_stream(app_name: str, confinement: str, root_password: str) -> SimpleProcess: install_cmd = [BASE_CMD, 'install', app_name] # default if confinement == 'classic': install_cmd.append('--classic') - return new_root_subprocess(install_cmd, root_password) + # return new_root_subprocess(install_cmd, root_password) + return SimpleProcess(install_cmd, root_password=root_password) def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen: diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 0942b49d..7175d8a7 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -67,7 +67,7 @@ class ComboBoxQt(QComboBox): class RadioSelectQt(QGroupBox): def __init__(self, model: SingleSelectComponent): - super(RadioSelectQt, self).__init__(model.label + ' :') + super(RadioSelectQt, self).__init__(model.label + ' :' if model.label else None) self.model = model self.setStyleSheet("QGroupBox { font-weight: bold }") diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 04523f1a..9e34ffbe 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -141,6 +141,9 @@ filetransfer=file transfer packagemanager=package manager sportsgame=sports game arcadegame=arcade game +player=player screenshots,download.running=downloading image screenshots.download.no_content=No content to display -screenshots.download.no_response=Image not found \ No newline at end of file +screenshots.download.no_response=Image not found +continue=continue +stable=stable \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index b9e055b8..78237bcb 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -182,8 +182,11 @@ filetransfer=transferencia de archivos packagemanager=administrador de paquetes sportsgame=juego de deportes arcadegame=juego arcade +player=jugador screenshots.bt_next.label=próxima screenshots.bt_back.label=anterior screenshots,download.running=descargando imagen screenshots.download.no_content=No hay contenido para mostrar -screenshots.download.no_response=Imagen no encontrada \ No newline at end of file +screenshots.download.no_response=Imagen no encontrada +continue=continuar +stable=estable \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 53a7a50d..045d61c7 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -185,8 +185,11 @@ viewer=visualizador packagemanager=gerenciador de pacotes sportsgame=jogo de esportes arcadegame=jogo arcade +player=jogador screenshots.bt_next.label=próxima screenshots.bt_back.label=anterior screenshots,download.running=baixando imagem screenshots.download.no_content=Sem conteúdo para exibir -screenshots.download.no_response=Imagem não encontrada \ No newline at end of file +screenshots.download.no_response=Imagem não encontrada +continue=continuar +stable=estável \ No newline at end of file From 8965d0a300a7181b7215c6c8c434726f55c36b16 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 12:04:25 -0300 Subject: [PATCH 17/46] [snap] i18n --- bauh/gems/snap/resources/locale/en | 2 +- bauh/gems/snap/resources/locale/es | 2 +- bauh/gems/snap/resources/locale/pt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 633cd875..354910ca 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -4,4 +4,4 @@ snap.notification.snap.disable=If you do not want to use Snap applications, unch snap.action.refresh.status=Refreshing snap.action.refresh.label=Refresh snap.install.available_channels.title=Available channels -snap.install.available_channels.body=There is no {} channel available for {}. But there are these others available below ( select one if you want to continue ) \ No newline at end of file +snap.install.available_channels.body=There is no {} channel available for {}. But there are these below ( select one if you want to continue ) \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index 0989e73b..9311ee4a 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -15,4 +15,4 @@ snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} e snap.action.refresh.status=Actualizando snap.action.refresh.label=Actualizar snap.install.available_channels.title=Canales disponibles -snap.install.available_channels.body=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros disponibles abajo ( seleccione uno si desea continuar ) \ No newline at end of file +snap.install.available_channels.body=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo ( seleccione uno si desea continuar ) \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index e93ae589..25b19832 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -15,4 +15,4 @@ snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque { snap.action.refresh.status=Atualizando snap.action.refresh.label=Atualizar snap.install.available_channels.title=Canais disponíveis -snap.install.available_channels.body=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros disponíveis abaixo ( selecione algum se quiser continuar ) \ No newline at end of file +snap.install.available_channels.body=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo ( selecione algum se quiser continuar ) \ No newline at end of file From 52f313032b86aaeb9371667bf53430e3de5fa7d4 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 12:34:09 -0300 Subject: [PATCH 18/46] fix: not loading application icons after some filters are applied to the table results --- CHANGELOG.md | 1 + bauh/view/qt/thread.py | 6 ++++++ bauh/view/qt/window.py | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bcd2e32..c376530a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - application not initializing when there is no internet connection +- not loading application icons after some filters are applied to the table results - AUR: - update-checking for some scenarios - not respecting **ignorepkg** settings in **pacman.conf** diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 75a95e12..2f96d799 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -352,6 +352,7 @@ class VerifyModels(QThread): def __init__(self, apps: List[PackageView] = None): super(VerifyModels, self).__init__() self.apps = apps + self.work = True def run(self): @@ -361,6 +362,10 @@ class VerifyModels(QThread): last_ready = 0 while True: + + if not self.work: + break + current_ready = 0 for app in self.apps: @@ -379,6 +384,7 @@ class VerifyModels(QThread): time.sleep(0.1) + self.work = True self.apps = None diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index cafe6280..c428f7a9 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -314,6 +314,12 @@ class ManageWindow(QWidget): self._update_table(pkgs_info=pkgs_info, signal=True) self.update_bt_upgrade(pkgs_info) + if self.pkgs_available: + self.thread_verify_models.work = False + self.thread_verify_models.wait(50) + self.thread_verify_models.apps = self.pkgs_available + self.thread_verify_models.start() + def _finish_apply_filters_async(self, success: bool): self.label_status.setText('') self.ref_toolbar_search.setVisible(True) From 9b17407067ac5b436a2388a2b3f68afbb03f7e33 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 13:20:31 -0300 Subject: [PATCH 19/46] [appimage] showing an error popup when AppImageLauncher messes up with an application installation --- CHANGELOG.md | 2 ++ bauh/gems/appimage/controller.py | 12 ++++++++++-- bauh/gems/appimage/resources/locale/en | 3 ++- bauh/gems/appimage/resources/locale/es | 3 ++- bauh/gems/appimage/resources/locale/pt | 3 ++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c376530a..0631d5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Improvements - 3 password attempts for root authentication +- AppImage: + - showing an error popup when **AppImageLauncher** messes up with an application installation - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 51ba3827..1d910b65 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -17,7 +17,7 @@ from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpda from bauh.api.abstract.view import MessageType from bauh.api.constants import HOME_PATH from bauh.commons.html import bold -from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd +from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess from bauh.gems.appimage import query, INSTALLATION_PATH, suggestions, db from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.worker import DatabaseUpdater @@ -298,12 +298,20 @@ class AppImageManager(SoftwareManager): watcher.change_substatus(self.i18n['appimage.install.extract'].format(bold(file_name))) try: - handler.handle(SystemProcess(new_subprocess([file_path, '--appimage-extract'], cwd=out_dir))) + res, output = handler.handle_simple(SimpleProcess([file_path, '--appimage-extract'], cwd=out_dir)) + + if 'Error: Failed to register AppImage in AppImageLauncherFS' in output: + watcher.show_message(title=self.i18n['error'], + body=self.i18n['appimage.install.appimagelauncher.error'].format(appimgl=bold('AppImageLauncher'), app=bold(pkg.name)), + type_=MessageType.ERROR) + handler.handle(SystemProcess(new_subprocess(['rm', '-rf', out_dir]))) + return False except: watcher.show_message(title=self.i18n['error'], body=traceback.format_exc(), type_=MessageType.ERROR) traceback.print_exc() + handler.handle(SystemProcess(new_subprocess(['rm', '-rf', out_dir]))) return False watcher.change_substatus(self.i18n['appimage.install.desktop_entry']) diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index e8a04789..0a4c5a07 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=It was not possible to identify {} curre appimage.downgrade.first_version={} is in its first published version appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} appimage.downgrade.install_version=It was not possible to install the version {} ({}) -appimage.install.download.error=It was not possible to download the file {}. The file server can be down. \ No newline at end of file +appimage.install.download.error=It was not possible to download the file {}. The file server can be down. +appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again. \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index ab80dbbe..932e3fa1 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=No fue posible identificar la versión a appimage.downgrade.first_version={} está en su primera versión publicada appimage.downgrade.uninstall_current_version=No fue posible desinstalar la versión actual de {} appimage.downgrade.install_version=No fue posible instalar la versión {} ({}) -appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo. \ No newline at end of file +appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo. +appimage.install.appimagelauncher.error = {appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente. \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 1f41b7dc..43ae6917 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=Não foi possível identificar a versão appimage.downgrade.first_version={} se encontra em sua primeira versão publicada appimage.downgrade.uninstall_current_version=Não foi possível desinstalar a versão atual de {} appimage.downgrade.install_version=Não foi possivel instalar a versão {} ({}) -appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar. \ No newline at end of file +appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar. +appimage.install.appimagelauncher.error={appimgl} não está permitindo que o {app} seja instalado. Desinstale o {appimgl}, reinicie o sistema e tente instalar o {app} novamente. \ No newline at end of file From 930308834b8b3b28a54c3e124e86a347f9da17ff Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 16:13:32 -0300 Subject: [PATCH 20/46] [aur] caching categories to the disk | not locking the application boot while the AUR categories are being retrieved --- CHANGELOG.md | 4 ++ bauh/gems/arch/__init__.py | 4 ++ bauh/gems/arch/controller.py | 24 ++++++------ bauh/gems/arch/mapper.py | 6 ++- bauh/gems/arch/model.py | 6 +-- bauh/gems/arch/worker.py | 73 +++++++++++++++++++++++++++++------- bauh/view/qt/screenshots.py | 2 +- 7 files changed, 87 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0631d5ab..3c32ccc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - showing an error popup when **AppImageLauncher** messes up with an application installation - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package + - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) + - caching cloud to categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) +- minor thread improvements ### Fixes - application not initializing when there is no internet connection @@ -24,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not respecting **ignorepkg** settings in **pacman.conf** - not able to handle **missing dependencies with symbols** ( e.g: libpng++ ) - not able to work with **.xpm** icons + - not mapping categories to the search results ## [0.7.0] 2019-10-18 ### Features diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 3b1cfda6..87c42aa7 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -1,4 +1,8 @@ import os + +from bauh.api.constants import CACHE_PATH + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) BUILD_DIR = '/tmp/bauh/aur' +ARCH_CACHE_PATH = CACHE_PATH + '/arch' diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 358e1a59..f4fd9a94 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -22,7 +22,7 @@ from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, c from bauh.gems.arch.aur import AURClient from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.model import ArchPackage -from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, CategoriesDownloader +from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, AURCategoriesMapper URL_GIT = 'https://aur.archlinux.org/{}.git' URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz' @@ -51,11 +51,11 @@ class ArchManager(SoftwareManager): self.logger = context.logger self.enabled = True self.arch_distro = context.distro == 'arch' - self.categories_downloader = CategoriesDownloader(context.http_client, context.logger) + self.categories_mapper = AURCategoriesMapper(context.http_client, context.logger, self, self.context.disk_cache) self.categories_map = {} def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader): - app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed']) + app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories_map) app.downgrade_enabled = downgrade_enabled if app.installed: @@ -66,7 +66,7 @@ class ArchManager(SoftwareManager): else: res.new.append(app) - Thread(target=self.mapper.fill_package_build, args=(app,)).start() + Thread(target=self.mapper.fill_package_build, args=(app,), daemon=True).start() def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult: self.comp_optimizer.join() @@ -75,7 +75,7 @@ class ArchManager(SoftwareManager): res = SearchResult([], [], 0) installed = {} - read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed())) + read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()), daemon=True) read_installed.start() api_res = self.aur_client.search(words) @@ -117,9 +117,8 @@ class ArchManager(SoftwareManager): if pkgsinfo: for pkgdata in pkgsinfo: - pkg = self.mapper.map_api_data(pkgdata, not_signed) + pkg = self.mapper.map_api_data(pkgdata, not_signed, self.categories_map) pkg.downgrade_enabled = downgrade_enabled - pkg.categories = self.categories_map.get(pkg.name) if disk_loader: disk_loader.fill(pkg) @@ -161,6 +160,7 @@ class ArchManager(SoftwareManager): apps = [] if installed and installed['not_signed']: self.dcache_updater.join() + self.categories_mapper.join() self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader, internet_available) @@ -684,7 +684,7 @@ class ArchManager(SoftwareManager): return False def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool): - pass + pass def requires_root(self, action: str, pkg: ArchPackage): return action != 'search' @@ -693,10 +693,7 @@ class ArchManager(SoftwareManager): self.dcache_updater.start() self.comp_optimizer.start() self.aur_index_updater.start() - categories = self.categories_downloader.get_categories() - - if categories: - self.categories_map = categories + self.categories_mapper.start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed @@ -731,9 +728,10 @@ class ArchManager(SoftwareManager): api_res = self.aur_client.get_info(sug_names) if api_res: + self.categories_mapper.join() for pkg in api_res: if pkg.get('Name') in sug_names: - res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}), suggestions.ALL.get(pkg['Name']))) + res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories_map), suggestions.ALL.get(pkg['Name']))) return res diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index d058d3c3..723c01ee 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -110,14 +110,18 @@ class ArchDataMapper: if res and res.status_code == 200 and res.text: pkg.pkgbuild = res.text - def map_api_data(self, apidata: dict, installed: dict) -> ArchPackage: + def map_api_data(self, apidata: dict, installed: dict, categories: dict) -> ArchPackage: data = installed.get(apidata.get('Name')) app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur') app.status = PackageStatus.LOADING_DATA + if categories: + app.categories = categories.get(app.name) + if data: app.version = data.get('version') app.description = data.get('description') self.fill_api_data(app, apidata, fill_version=not data) + return app diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index 9003f983..9da563eb 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -2,10 +2,8 @@ import datetime from typing import List from bauh.api.abstract.model import SoftwarePackage -from bauh.api.constants import CACHE_PATH from bauh.commons import resource - -from bauh.gems.arch import ROOT_DIR +from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry'} @@ -35,7 +33,7 @@ class ArchPackage(SoftwarePackage): @staticmethod def disk_cache_path(pkgname: str, mirror: str): - return CACHE_PATH + '/arch/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname + return ARCH_CACHE_PATH + '/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname def get_pkg_build_url(self): if self.package_base: diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index a0f69558..b5fa9c05 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -5,6 +5,7 @@ import time import traceback from math import ceil from multiprocessing import Process +from pathlib import Path from threading import Thread from typing import Dict, List @@ -12,9 +13,9 @@ import requests from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager -from bauh.api.constants import HOME_PATH +from bauh.api.constants import HOME_PATH, CACHE_PATH from bauh.api.http import HttpClient -from bauh.gems.arch import pacman, disk +from bauh.gems.arch import pacman, disk, ARCH_CACHE_PATH URL_INDEX = 'https://aur.archlinux.org/packages.gz' URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}' @@ -139,13 +140,51 @@ class ArchCompilationOptimizer(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else P self.logger.warning("A custom 'makepkg.conf' is already defined at '{}'".format(HOME_PATH)) -class CategoriesDownloader: +class AURCategoriesMapper(Thread): URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt' + CATEGORIES_CACHE_DIR = ARCH_CACHE_PATH + '/categories' + CATEGORIES_FILE_PATH = CATEGORIES_CACHE_DIR + '/aur.txt' - def __init__(self, http_client: HttpClient, logger: logging.Logger): + def __init__(self, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, disk_cache: bool): + super(AURCategoriesMapper, self).__init__(daemon=True) self.http_client = http_client self.logger = logger + self.manager = manager + self.disk_cache = disk_cache + + def _read_categories_from_disk(self) -> dict: + if self.disk_cache and os.path.exists(self.CATEGORIES_FILE_PATH): + self.logger.info("Reading cached categories from the disk") + + with open(self.CATEGORIES_FILE_PATH) as f: + categories = f.read() + + return self._map_categories(categories) + + return {} + + def _map_categories(self, categories: str) -> dict: + categories_map = {} + for l in categories.split('\n'): + if l: + data = l.split('=') + categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + return categories_map + + def _cache_categories_to_disk(self, categories: str): + self.logger.info('Caching AUR categories to the disk') + + try: + Path(self.CATEGORIES_CACHE_DIR).mkdir(parents=True, exist_ok=True) + + with open(self.CATEGORIES_FILE_PATH, 'w+') as f: + f.write(categories) + + self.logger.info("AUR categories cached to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) + except: + self.logger.error("Could not cache AUR categories to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) + traceback.print_exc() def get_categories(self) -> Dict[str, List[str]]: self.logger.info('Downloading AUR category definitions from {}'.format(self.URL_CATEGORIES_FILE)) @@ -155,21 +194,29 @@ class CategoriesDownloader: if res: try: - categories_map = {} - for l in res.text.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] - + categories_map = self._map_categories(res.text) self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map))) + + if self.disk_cache and categories_map: + t = Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True) + t.start() + return categories_map except: self.logger.error("Could not parse AUR categories definitions") traceback.print_exc() - return {} else: self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) - return {} + except requests.exceptions.ConnectionError: self.logger.warning('The internet connection seems to be off.') - return {} + + return self._read_categories_from_disk() + + def run(self): + categories = self.get_categories() + + if categories: + self.logger.info("Settings categories to {}".format(self.manager.__class__.__name__)) + self.manager.categories_map = categories + self.logger.info('Finished') diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py index 8f8dea43..395e89b0 100644 --- a/bauh/view/qt/screenshots.py +++ b/bauh/view/qt/screenshots.py @@ -64,7 +64,7 @@ class ScreenshotsDialog(QDialog): self.img_idx = 0 for idx, s in enumerate(self.screenshots): - t = Thread(target=self._download_img, args=(idx, s)) + t = Thread(target=self._download_img, args=(idx, s), daemon=True) t.start() self._load_img() From c358d077be981964721f8d5aa4b0640564577634 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 16:36:10 -0300 Subject: [PATCH 21/46] [snap] caching categories to the disk | not locking the application boot while categories are being retrieved --- CHANGELOG.md | 3 ++ bauh/gems/arch/controller.py | 10 ++--- bauh/gems/arch/worker.py | 8 ++-- bauh/gems/snap/__init__.py | 4 ++ bauh/gems/snap/controller.py | 11 +++--- bauh/gems/snap/worker.py | 72 ++++++++++++++++++++++++++++++------ 6 files changed, 82 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c32ccc7..259e6710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud to categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) +- Snap: + - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) + - caching cloud to categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) - minor thread improvements ### Fixes diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index f4fd9a94..37a645bd 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -52,10 +52,10 @@ class ArchManager(SoftwareManager): self.enabled = True self.arch_distro = context.distro == 'arch' self.categories_mapper = AURCategoriesMapper(context.http_client, context.logger, self, self.context.disk_cache) - self.categories_map = {} + self.categories = {} def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader): - app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories_map) + app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories) app.downgrade_enabled = downgrade_enabled if app.installed: @@ -117,7 +117,7 @@ class ArchManager(SoftwareManager): if pkgsinfo: for pkgdata in pkgsinfo: - pkg = self.mapper.map_api_data(pkgdata, not_signed, self.categories_map) + pkg = self.mapper.map_api_data(pkgdata, not_signed, self.categories) pkg.downgrade_enabled = downgrade_enabled if disk_loader: @@ -136,7 +136,7 @@ class ArchManager(SoftwareManager): latest_version=data.get('version'), description=data.get('description'), installed=True, mirror='aur') - pkg.categories = self.categories_map.get(pkg.name) + pkg.categories = self.categories.get(pkg.name) pkg.downgrade_enabled = downgrade_enabled if disk_loader: @@ -731,7 +731,7 @@ class ArchManager(SoftwareManager): self.categories_mapper.join() for pkg in api_res: if pkg.get('Name') in sug_names: - res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories_map), suggestions.ALL.get(pkg['Name']))) + res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories), suggestions.ALL.get(pkg['Name']))) return res diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index b5fa9c05..1a359d87 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -198,8 +198,7 @@ class AURCategoriesMapper(Thread): self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map))) if self.disk_cache and categories_map: - t = Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True) - t.start() + Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() return categories_map except: @@ -218,5 +217,6 @@ class AURCategoriesMapper(Thread): if categories: self.logger.info("Settings categories to {}".format(self.manager.__class__.__name__)) - self.manager.categories_map = categories - self.logger.info('Finished') + self.manager.categories = categories + + self.logger.info('Finished') diff --git a/bauh/gems/snap/__init__.py b/bauh/gems/snap/__init__.py index 8824c2d4..6e7f3aaf 100644 --- a/bauh/gems/snap/__init__.py +++ b/bauh/gems/snap/__init__.py @@ -1,2 +1,6 @@ import os + +from bauh.api.constants import CACHE_PATH + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +SNAP_CACHE_PATH = CACHE_PATH + '/snap' diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 0ab35e23..006f3cdb 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -19,6 +19,7 @@ from bauh.gems.snap.worker import SnapAsyncDataLoader, CategoriesDownloader RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)')) + class SnapManager(SoftwareManager): def __init__(self, context: ApplicationContext): @@ -31,7 +32,7 @@ class SnapManager(SoftwareManager): self.logger = context.logger self.ubuntu_distro = context.distro == 'ubuntu' self.categories = {} - self.categories_downloader = CategoriesDownloader(self.http_client, self.logger) + self.categories_downloader = CategoriesDownloader(self.http_client, self.logger, self, context.disk_cache) def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication: app = SnapApplication(publisher=app_json.get('publisher'), @@ -91,6 +92,7 @@ class SnapManager(SoftwareManager): def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: if snap.is_snapd_running(): + self.categories_downloader.join() installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader, internet=internet_available) for app_json in snap.read_installed(self.ubuntu_distro)] return SearchResult(installed, None, len(installed)) else: @@ -173,10 +175,7 @@ class SnapManager(SoftwareManager): return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password))) def prepare(self): - categories = self.categories_downloader.get_categories() - - if categories: - self.categories = categories + self.categories_downloader.start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: pass @@ -207,6 +206,8 @@ class SnapManager(SoftwareManager): sugs.sort(key=lambda t: t[1].value, reverse=True) threads = [] + self.categories_downloader.join() + for sug in sugs: if limit <= 0 or len(res) < limit: diff --git a/bauh/gems/snap/worker.py b/bauh/gems/snap/worker.py index 54bd1271..17a609d0 100644 --- a/bauh/gems/snap/worker.py +++ b/bauh/gems/snap/worker.py @@ -1,6 +1,7 @@ import logging import os import traceback +from pathlib import Path from threading import Thread from typing import Dict, List @@ -11,7 +12,7 @@ from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.model import PackageStatus from bauh.api.http import HttpClient -from bauh.gems.snap import snap +from bauh.gems.snap import snap, SNAP_CACHE_PATH from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.model import SnapApplication @@ -76,13 +77,51 @@ class SnapAsyncDataLoader(Thread): self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False) -class CategoriesDownloader: +class CategoriesDownloader(Thread): URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt' + CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt' - def __init__(self, http_client: HttpClient, logger: logging.Logger): + def __init__(self, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, disk_cache: bool): + super(CategoriesDownloader, self).__init__(daemon=True) self.http_client = http_client self.logger = logger + self.manager = manager + self.disk_cache = disk_cache + + def _read_categories_from_disk(self) -> dict: + if self.disk_cache and os.path.exists(self.CATEGORIES_FILE_PATH): + self.logger.info("Reading cached categories from the disk") + + with open(self.CATEGORIES_FILE_PATH) as f: + categories = f.read() + + return self._map_categories(categories) + + return {} + + def _map_categories(self, categories: str) -> dict: + categories_map = {} + for l in categories.split('\n'): + if l: + data = l.split('=') + categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + + return categories_map + + def _cache_categories_to_disk(self, categories: str): + self.logger.info('Caching Snap categories to the disk') + + try: + Path(SNAP_CACHE_PATH).mkdir(parents=True, exist_ok=True) + + with open(self.CATEGORIES_FILE_PATH, 'w+') as f: + f.write(categories) + + self.logger.info("Snap categories cached to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) + except: + self.logger.error("Could not cache Snap categories to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) + traceback.print_exc() def get_categories(self) -> Dict[str, List[str]]: self.logger.info('Downloading Snap category definitions from {}'.format(self.URL_CATEGORIES_FILE)) @@ -92,20 +131,29 @@ class CategoriesDownloader: if res: try: - categories_map = {} - for l in res.text.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + categories = self._map_categories(res.text) + self.logger.info('Loaded categories for {} Snap applications'.format(len(categories))) - self.logger.info('Loaded categories for {} Snap applications'.format(len(categories_map))) - return categories_map + if self.disk_cache and categories: + Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() + + return categories except: self.logger.error("Could not parse categories definitions") traceback.print_exc() - return {} else: self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) + except requests.exceptions.ConnectionError: self.logger.warning('The internet connection seems to be off.') - return {} + + return self._read_categories_from_disk() + + def run(self): + categories = self.get_categories() + + if categories: + self.logger.info("Settings categories to {}".format(self.manager.__class__.__name__)) + self.manager.categories = categories + + self.logger.info('Finished') From 64e4822b2d23fde6b3796e16bc1a487984f9c6f7 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 17:10:24 -0300 Subject: [PATCH 22/46] [refactoring] CategoriesDownloader as a common class --- bauh/commons/category.py | 99 ++++++++++++++++++++++++++++++++++++ bauh/gems/arch/__init__.py | 4 +- bauh/gems/arch/controller.py | 9 ++-- bauh/gems/arch/worker.py | 90 +------------------------------- bauh/gems/snap/__init__.py | 2 + bauh/gems/snap/controller.py | 8 +-- bauh/gems/snap/worker.py | 91 +-------------------------------- 7 files changed, 118 insertions(+), 185 deletions(-) create mode 100644 bauh/commons/category.py diff --git a/bauh/commons/category.py b/bauh/commons/category.py new file mode 100644 index 00000000..0f9e3831 --- /dev/null +++ b/bauh/commons/category.py @@ -0,0 +1,99 @@ +import logging +import os +import traceback +from pathlib import Path +from threading import Thread +from typing import Dict, List + +import requests + +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.http import HttpClient + + +class CategoriesDownloader(Thread): + + def __init__(self, id_: str, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, + disk_cache: bool, url_categories_file: str, disk_cache_dir: str, categories_path: str): + super(CategoriesDownloader, self).__init__(daemon=True) + self.id_ = id_ + self.http_client = http_client + self.logger = logger + self.manager = manager + self.disk_cache = disk_cache + self.url_categories_file = url_categories_file + self.disk_cache_dir = disk_cache_dir + self.categories_path = categories_path + + def _msg(self, msg: str): + return '{}({}): {}'.format(self.__class__.__name__, self.id_, msg) + + def _read_categories_from_disk(self) -> Dict[str, List[str]]: + if self.disk_cache and os.path.exists(self.categories_path): + self.logger.info(self._msg("Reading cached categories from the disk")) + + with open(self.categories_path) as f: + categories = f.read() + + return self._map_categories(categories) + + return {} + + def _map_categories(self, categories: str) -> Dict[str, List[str]]: + categories_map = {} + for l in categories.split('\n'): + if l: + data = l.split('=') + categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] + + return categories_map + + def _cache_categories_to_disk(self, categories: str): + self.logger.info(self._msg('Caching categories to the disk')) + + try: + Path(self.disk_cache_dir).mkdir(parents=True, exist_ok=True) + + with open(self.categories_path, 'w+') as f: + f.write(categories) + + self.logger.info(self._msg("Categories cached to the disk as '{}'".format(self.categories_path))) + except: + self.logger.error(self._msg("Could not cache categories to the disk as '{}'".format(self.categories_path))) + traceback.print_exc() + + def download_categories(self) -> Dict[str, List[str]]: + self.logger.info(self._msg('Downloading category definitions from {}'.format(self.url_categories_file))) + + try: + res = self.http_client.get(self.url_categories_file) + + if res: + try: + categories = self._map_categories(res.text) + self.logger.info(self._msg('Loaded categories for {} applications'.format(len(categories)))) + + if self.disk_cache and categories: + Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() + + return categories + except: + self.logger.error(self._msg("Could not parse categories definitions")) + traceback.print_exc() + else: + self.logger.info(self._msg('Could not download {}'.format(self.url_categories_file))) + + except requests.exceptions.ConnectionError: + self.logger.warning(self._msg('The internet connection seems to be off.')) + + return {} + + def _set_categories(self, categories: dict): + if categories: + self.logger.info(self._msg("Settings categories to {}".format(self.manager.__class__.__name__))) + self.manager.categories = categories + + def run(self): + self._set_categories(self._read_categories_from_disk()) + self._set_categories(self.download_categories()) + self.logger.info(self._msg('Finished')) diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 87c42aa7..5bd71924 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -5,4 +5,6 @@ from bauh.api.constants import CACHE_PATH ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) BUILD_DIR = '/tmp/bauh/aur' ARCH_CACHE_PATH = CACHE_PATH + '/arch' - +CATEGORIES_CACHE_DIR = ARCH_CACHE_PATH + '/categories' +CATEGORIES_FILE_PATH = CATEGORIES_CACHE_DIR + '/aur.txt' +URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt' diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 37a645bd..2b7934a2 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -15,14 +15,16 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus from bauh.api.abstract.view import MessageType +from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ SimpleProcess -from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions, gpg +from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions, \ + gpg, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH from bauh.gems.arch.aur import AURClient from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.model import ArchPackage -from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, AURCategoriesMapper +from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer URL_GIT = 'https://aur.archlinux.org/{}.git' URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz' @@ -51,7 +53,8 @@ class ArchManager(SoftwareManager): self.logger = context.logger self.enabled = True self.arch_distro = context.distro == 'arch' - self.categories_mapper = AURCategoriesMapper(context.http_client, context.logger, self, self.context.disk_cache) + self.categories_mapper = CategoriesDownloader('AUR', context.http_client, context.logger, self, self.context.disk_cache, + URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH) self.categories = {} def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader): diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 1a359d87..b7f9245f 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -2,20 +2,16 @@ import logging import os import re import time -import traceback from math import ceil from multiprocessing import Process -from pathlib import Path from threading import Thread -from typing import Dict, List import requests from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager -from bauh.api.constants import HOME_PATH, CACHE_PATH -from bauh.api.http import HttpClient -from bauh.gems.arch import pacman, disk, ARCH_CACHE_PATH +from bauh.api.constants import HOME_PATH +from bauh.gems.arch import pacman, disk URL_INDEX = 'https://aur.archlinux.org/packages.gz' URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}' @@ -138,85 +134,3 @@ class ArchCompilationOptimizer(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else P self.logger.info("A custom optimized 'makepkg.conf' was generated at '{}'".format(HOME_PATH)) else: self.logger.warning("A custom 'makepkg.conf' is already defined at '{}'".format(HOME_PATH)) - - -class AURCategoriesMapper(Thread): - - URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt' - CATEGORIES_CACHE_DIR = ARCH_CACHE_PATH + '/categories' - CATEGORIES_FILE_PATH = CATEGORIES_CACHE_DIR + '/aur.txt' - - def __init__(self, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, disk_cache: bool): - super(AURCategoriesMapper, self).__init__(daemon=True) - self.http_client = http_client - self.logger = logger - self.manager = manager - self.disk_cache = disk_cache - - def _read_categories_from_disk(self) -> dict: - if self.disk_cache and os.path.exists(self.CATEGORIES_FILE_PATH): - self.logger.info("Reading cached categories from the disk") - - with open(self.CATEGORIES_FILE_PATH) as f: - categories = f.read() - - return self._map_categories(categories) - - return {} - - def _map_categories(self, categories: str) -> dict: - categories_map = {} - for l in categories.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] - return categories_map - - def _cache_categories_to_disk(self, categories: str): - self.logger.info('Caching AUR categories to the disk') - - try: - Path(self.CATEGORIES_CACHE_DIR).mkdir(parents=True, exist_ok=True) - - with open(self.CATEGORIES_FILE_PATH, 'w+') as f: - f.write(categories) - - self.logger.info("AUR categories cached to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) - except: - self.logger.error("Could not cache AUR categories to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) - traceback.print_exc() - - def get_categories(self) -> Dict[str, List[str]]: - self.logger.info('Downloading AUR category definitions from {}'.format(self.URL_CATEGORIES_FILE)) - - try: - res = self.http_client.get(self.URL_CATEGORIES_FILE) - - if res: - try: - categories_map = self._map_categories(res.text) - self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map))) - - if self.disk_cache and categories_map: - Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() - - return categories_map - except: - self.logger.error("Could not parse AUR categories definitions") - traceback.print_exc() - else: - self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) - - except requests.exceptions.ConnectionError: - self.logger.warning('The internet connection seems to be off.') - - return self._read_categories_from_disk() - - def run(self): - categories = self.get_categories() - - if categories: - self.logger.info("Settings categories to {}".format(self.manager.__class__.__name__)) - self.manager.categories = categories - - self.logger.info('Finished') diff --git a/bauh/gems/snap/__init__.py b/bauh/gems/snap/__init__.py index 6e7f3aaf..bd219398 100644 --- a/bauh/gems/snap/__init__.py +++ b/bauh/gems/snap/__init__.py @@ -4,3 +4,5 @@ from bauh.api.constants import CACHE_PATH ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) SNAP_CACHE_PATH = CACHE_PATH + '/snap' +CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt' +URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt' diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 006f3cdb..2ecdd7c1 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -10,12 +10,13 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption +from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess -from bauh.gems.snap import snap, suggestions +from bauh.gems.snap import snap, suggestions, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.model import SnapApplication -from bauh.gems.snap.worker import SnapAsyncDataLoader, CategoriesDownloader +from bauh.gems.snap.worker import SnapAsyncDataLoader RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)')) @@ -32,7 +33,8 @@ class SnapManager(SoftwareManager): self.logger = context.logger self.ubuntu_distro = context.distro == 'ubuntu' self.categories = {} - self.categories_downloader = CategoriesDownloader(self.http_client, self.logger, self, context.disk_cache) + self.categories_downloader = CategoriesDownloader('snap', self.http_client, self.logger, self, context.disk_cache, + URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH) def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication: app = SnapApplication(publisher=app_json.get('publisher'), diff --git a/bauh/gems/snap/worker.py b/bauh/gems/snap/worker.py index 17a609d0..72dd76c2 100644 --- a/bauh/gems/snap/worker.py +++ b/bauh/gems/snap/worker.py @@ -1,18 +1,11 @@ -import logging -import os import traceback -from pathlib import Path from threading import Thread -from typing import Dict, List - -import requests from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.model import PackageStatus -from bauh.api.http import HttpClient -from bauh.gems.snap import snap, SNAP_CACHE_PATH +from bauh.gems.snap import snap from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.model import SnapApplication @@ -75,85 +68,3 @@ class SnapAsyncDataLoader(Thread): if self.persist: self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False) - - -class CategoriesDownloader(Thread): - - URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt' - CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt' - - def __init__(self, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, disk_cache: bool): - super(CategoriesDownloader, self).__init__(daemon=True) - self.http_client = http_client - self.logger = logger - self.manager = manager - self.disk_cache = disk_cache - - def _read_categories_from_disk(self) -> dict: - if self.disk_cache and os.path.exists(self.CATEGORIES_FILE_PATH): - self.logger.info("Reading cached categories from the disk") - - with open(self.CATEGORIES_FILE_PATH) as f: - categories = f.read() - - return self._map_categories(categories) - - return {} - - def _map_categories(self, categories: str) -> dict: - categories_map = {} - for l in categories.split('\n'): - if l: - data = l.split('=') - categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c] - - return categories_map - - def _cache_categories_to_disk(self, categories: str): - self.logger.info('Caching Snap categories to the disk') - - try: - Path(SNAP_CACHE_PATH).mkdir(parents=True, exist_ok=True) - - with open(self.CATEGORIES_FILE_PATH, 'w+') as f: - f.write(categories) - - self.logger.info("Snap categories cached to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) - except: - self.logger.error("Could not cache Snap categories to the disk as '{}'".format(self.CATEGORIES_FILE_PATH)) - traceback.print_exc() - - def get_categories(self) -> Dict[str, List[str]]: - self.logger.info('Downloading Snap category definitions from {}'.format(self.URL_CATEGORIES_FILE)) - - try: - res = self.http_client.get(self.URL_CATEGORIES_FILE) - - if res: - try: - categories = self._map_categories(res.text) - self.logger.info('Loaded categories for {} Snap applications'.format(len(categories))) - - if self.disk_cache and categories: - Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() - - return categories - except: - self.logger.error("Could not parse categories definitions") - traceback.print_exc() - else: - self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE)) - - except requests.exceptions.ConnectionError: - self.logger.warning('The internet connection seems to be off.') - - return self._read_categories_from_disk() - - def run(self): - categories = self.get_categories() - - if categories: - self.logger.info("Settings categories to {}".format(self.manager.__class__.__name__)) - self.manager.categories = categories - - self.logger.info('Finished') From 5df45627eee1e94726757a465707023f42f893af Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 17:24:39 -0300 Subject: [PATCH 23/46] [aur] caching the package categories to the disk --- CHANGELOG.md | 4 ++-- bauh/gems/arch/controller.py | 2 +- bauh/gems/arch/disk.py | 5 ++++- bauh/gems/arch/model.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 259e6710..ac702efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - - caching cloud to categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) + - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) - Snap: - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - - caching cloud to categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) + - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) - minor thread improvements ### Fixes diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 2b7934a2..e3f811be 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -568,7 +568,7 @@ class ArchManager(SoftwareManager): if installed and self.context.disk_cache: handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname))) if self.context.disk_cache: - disk.save_several({pkgname}, mirror=mirror, maintainer=maintainer, overwrite=True) + disk.save_several({pkgname}, mirror=mirror, maintainer=maintainer, overwrite=True, categories=self.categories) self._update_progress(handler.watcher, 100, change_progress) diff --git a/bauh/gems/arch/disk.py b/bauh/gems/arch/disk.py index 9e012f05..124a6a66 100644 --- a/bauh/gems/arch/disk.py +++ b/bauh/gems/arch/disk.py @@ -47,7 +47,7 @@ def set_icon_path(app: ArchPackage, icon_name: str = None): break -def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, maintainer: str = None) -> int: +def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, maintainer: str = None, categories: dict = None) -> int: to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))} desktop_files = pacman.list_desktop_entries(to_cache) @@ -138,6 +138,9 @@ def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, mainta if to_write: for p in to_write: + if categories: + p.categories = categories.get(p.name) + p.maintainer = maintainer write(p) return len(to_write) diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index 9da563eb..62d6d5fb 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -5,7 +5,7 @@ from bauh.api.abstract.model import SoftwarePackage from bauh.commons import resource from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH -CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry'} +CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry', 'categories'} class ArchPackage(SoftwarePackage): From 773c761493732a9b779b2bd25160a1049c203491 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 22 Oct 2019 17:43:57 -0300 Subject: [PATCH 24/46] [categories] not locking the boot when could read from the disk cache --- bauh/commons/category.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bauh/commons/category.py b/bauh/commons/category.py index 0f9e3831..bbf4bc29 100644 --- a/bauh/commons/category.py +++ b/bauh/commons/category.py @@ -90,10 +90,19 @@ class CategoriesDownloader(Thread): def _set_categories(self, categories: dict): if categories: - self.logger.info(self._msg("Settings categories to {}".format(self.manager.__class__.__name__))) + self.logger.info(self._msg("Settings {} categories to {}".format(len(categories), self.manager.__class__.__name__))) self.manager.categories = categories - def run(self): - self._set_categories(self._read_categories_from_disk()) + def _download_and_set(self): self._set_categories(self.download_categories()) + + def run(self): + cached = self._read_categories_from_disk() + + if cached: + self._set_categories(cached) + Thread(target=self._download_and_set, daemon=True).start() + else: + self._download_and_set() + self.logger.info(self._msg('Finished')) From 31f2a3acab7b398b2ec41f08b466e4d15491fb35 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 12:11:18 -0300 Subject: [PATCH 25/46] [screenshots] progress bar --- CHANGELOG.md | 4 ++++ bauh/gems/flatpak/controller.py | 17 ++++++++++++----- bauh/view/qt/screenshots.py | 24 +++++++++++++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac702efa..3c1fa1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) - minor thread improvements + +### UI +- Screenshots panel: + - "downloading" label replaced by a progress bar ### Fixes - application not initializing when there is no internet connection diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index a431f4c3..43445cb4 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -1,3 +1,4 @@ +import json import traceback from datetime import datetime from threading import Thread @@ -279,11 +280,17 @@ class FlatpakManager(SoftwareManager): flatpak.run(str(pkg.id)) def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: - res = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, pkg.id)) + screenshots_url = '{}/apps/{}'.format(FLATHUB_API_URL, pkg.id) urls = [] - if res and res.get('screenshots'): - for s in res['screenshots']: - if s.get('imgDesktopUrl'): - urls.append(s['imgDesktopUrl']) + try: + res = self.http_client.get_json(screenshots_url) + + if res and res.get('screenshots'): + for s in res['screenshots']: + if s.get('imgDesktopUrl'): + urls.append(s['imgDesktopUrl']) + + except json.decoder.JSONDecodeError: + self.context.logger.error("Could not decode json from '{}'".format(screenshots_url)) return urls diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py index 395e89b0..56bd13fc 100644 --- a/bauh/view/qt/screenshots.py +++ b/bauh/view/qt/screenshots.py @@ -4,12 +4,13 @@ from typing import List from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QToolBar, QVBoxLayout +from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QToolBar, QVBoxLayout, QProgressBar, QApplication from bauh.api.abstract.cache import MemoryCache from bauh.api.http import HttpClient from bauh.view.qt import qt_utils from bauh.view.qt.components import new_spacer +from bauh.view.qt.thread import AnimateProgress from bauh.view.qt.view_model import PackageView @@ -28,6 +29,12 @@ class ScreenshotsDialog(QDialog): self.resize(1280, 720) self.i18n = i18n self.http_client = http_client + self.progress_bar = QProgressBar() + self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 6) + self.progress_bar.setTextVisible(False) + self.thread_progress = AnimateProgress() + self.thread_progress.signal_change.connect(self._update_progress) + self.thread_progress.start() # THERE ARE CRASHES WITH SOME RARE ICONS ( like insomnia ). IT CAN BE A QT BUG. IN THE MEANTIME, ONLY THE TYPE ICON WILL BE RENDERED # @@ -52,7 +59,9 @@ class ScreenshotsDialog(QDialog): self.img_label = QLabel() self.img_label.setStyleSheet('QLabel { font-weight: bold; text-align: center }') - self.bottom_bar.addWidget(self.img_label) + self.ref_img_label = self.bottom_bar.addWidget(self.img_label) + self.ref_img_label.setVisible(False) + self.ref_progress_bar = self.bottom_bar.addWidget(self.progress_bar) self.bottom_bar.addWidget(new_spacer(50)) self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize()) @@ -69,6 +78,9 @@ class ScreenshotsDialog(QDialog): self._load_img() + def _update_progress(self, val: int): + self.progress_bar.setValue(val) + def _load_img(self): if len(self.loaded_imgs) > self.img_idx: img = self.loaded_imgs[self.img_idx] @@ -79,9 +91,15 @@ class ScreenshotsDialog(QDialog): else: self.img_label.setText(img) self.img.setPixmap(QPixmap()) + + self.thread_progress.stop = True + self.ref_progress_bar.setVisible(False) + self.ref_img_label.setVisible(True) else: self.img.setPixmap(QPixmap()) - self.img_label.setText('...{}...'.format(self.i18n['screenshots,download.running'])) + self.ref_img_label.setVisible(False) + self.ref_progress_bar.setVisible(True) + self.thread_progress.start() if len(self.screenshots) == 1: self.ref_bt_back.setVisible(False) From 51cfd9145583d2a003e25bcaba6076bc49537e1f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 12:32:10 -0300 Subject: [PATCH 26/46] [snap] showing a warning popup when the Snap API is out --- CHANGELOG.md | 1 + bauh/gems/snap/controller.py | 6 ++++++ bauh/gems/snap/resources/locale/en | 1 + bauh/gems/snap/resources/locale/es | 1 + bauh/gems/snap/resources/locale/pt | 1 + bauh/gems/snap/snap.py | 14 +++++++++++++- 6 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1fa1b1..79c2c9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Snap: - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) + - showing a warning popup when the Snap API is out - minor thread improvements ### UI diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 2ecdd7c1..063f5575 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -188,6 +188,12 @@ class SnapManager(SoftwareManager): return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), self.i18n['snap.notification.snap.disable'].format(snap_bold, bold(self.i18n['manage_window.settings.gems']))] + available, output = snap.is_api_available() + + if not available: + self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) + return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] + def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority, out: List[PackageSuggestion]): res = self.http_client.get_json(SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name)) diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 354910ca..cfe914b8 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -1,6 +1,7 @@ gem.snap.info=Applications published at https://snapcraft.io/store snap.notification.snapd_unavailable={} seems not to be installed or enabled. {} packages will not be available. snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {} +snap.notifications.api.unavailable=It seems the {} API is unavailable at the moment. It will not be possible to search for new {} applications. snap.action.refresh.status=Refreshing snap.action.refresh.label=Refresh snap.install.available_channels.title=Available channels diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index 9311ee4a..bf403bdc 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -12,6 +12,7 @@ snap.info.version=versión snap.info.size=tamaño snap.notification.snapd_unavailable={} no parece estar instalado o habilitado. Los paquetes {} estarán indisponibles. snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} +snap.notifications.api.unavailable = Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}}. snap.action.refresh.status=Actualizando snap.action.refresh.label=Actualizar snap.install.available_channels.title=Canales disponibles diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 25b19832..d705e1f3 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -12,6 +12,7 @@ snap.info.version=versão snap.info.size=tamanho snap.notification.snapd_unavailable={} não parece estar instalado ou habilitado. Os pacotes {} estarão indisponíveis. snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {} +snap.notifications.api.unavailable=Parece que a API de {} está indisponível no momento. Não será possível buscar novos aplicativos {}. snap.action.refresh.status=Atualizando snap.action.refresh.label=Atualizar snap.install.available_channels.title=Canais disponíveis diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index b2c252cc..ad73dcc1 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -1,7 +1,8 @@ import logging import re import subprocess -from typing import List +from io import StringIO +from typing import List, Tuple from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess, SimpleProcess from bauh.gems.snap.model import SnapApplication @@ -227,3 +228,14 @@ def run(app: SnapApplication, logger: logging.Logger): logger.error("No valid command found for '{}'".format(app_name)) else: logger.error("No command found for '{}'".format(app_name)) + + +def is_api_available() -> Tuple[bool, str]: + output = StringIO() + for o in SimpleProcess(['snap', 'search']).instance.stdout: + if o: + output.write(o.decode()) + + output.seek(0) + output = output.read() + return 'error:' not in output, output From 270cf0e1d121fc2c0d6678e25cf1b5f80a82d8ac Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 12:42:41 -0300 Subject: [PATCH 27/46] [i18n] es - fix --- bauh/gems/snap/resources/locale/es | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index bf403bdc..030edd69 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -12,7 +12,7 @@ snap.info.version=versión snap.info.size=tamaño snap.notification.snapd_unavailable={} no parece estar instalado o habilitado. Los paquetes {} estarán indisponibles. snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} -snap.notifications.api.unavailable = Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}}. +snap.notifications.api.unavailable=Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}. snap.action.refresh.status=Actualizando snap.action.refresh.label=Actualizar snap.install.available_channels.title=Canales disponibles From 24beb25fca1c9f4e0b7fa96f79b14aeafe2cb54d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 17:24:12 -0300 Subject: [PATCH 28/46] not changing the table applied filters after a uninstall | [flatpak] runtimes categorized as 'runtimes' --- CHANGELOG.md | 3 ++ bauh/gems/flatpak/model.py | 3 ++ bauh/view/qt/about.py | 2 +- bauh/view/qt/window.py | 97 ++++++++++++++++++++++++-------------- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c2c9cf..148f8379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Improvements - 3 password attempts for root authentication +- not changing the table applied filters after a uninstall - AppImage: - showing an error popup when **AppImageLauncher** messes up with an application installation +- Flatpak: + - Runtimes now are categorized as "runtimes" - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) diff --git a/bauh/gems/flatpak/model.py b/bauh/gems/flatpak/model.py index bbddd94c..50e70665 100644 --- a/bauh/gems/flatpak/model.py +++ b/bauh/gems/flatpak/model.py @@ -17,6 +17,9 @@ class FlatpakApplication(SoftwarePackage): self.runtime = runtime self.commit = commit + if runtime: + self.categories = ['runtime'] + def is_incomplete(self): return self.description is None and self.icon_url diff --git a/bauh/view/qt/about.py b/bauh/view/qt/about.py index 1d2f0679..9f730fdd 100644 --- a/bauh/view/qt/about.py +++ b/bauh/view/qt/about.py @@ -55,7 +55,7 @@ class AboutDialog(QDialog): layout.addWidget(gems_widget) layout.addWidget(QLabel('')) - label_version = QLabel(locale_keys['version'].lower() + ': ' + __version__) + label_version = QLabel(locale_keys['version'].lower() + ' ' + __version__) label_version.setStyleSheet('QLabel { font-size: 10px; font-weight: bold }') label_version.setAlignment(Qt.AlignCenter) layout.addWidget(label_version) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index c428f7a9..a243cdc8 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -19,8 +19,6 @@ from bauh.api.http import HttpClient from bauh.commons.html import bold from bauh.view.core.config import Configuration from bauh.view.core.controller import GenericSoftwareManager -from bauh.view.qt.screenshots import ScreenshotsDialog -from bauh.view.util import util, resource from bauh.view.qt import dialog, commons, qt_utils from bauh.view.qt.about import AboutDialog from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton @@ -30,12 +28,14 @@ from bauh.view.qt.gem_selector import GemSelectorPanel from bauh.view.qt.history import HistoryDialog from bauh.view.qt.info import InfoDialog from bauh.view.qt.root import is_root, ask_root_password +from bauh.view.qt.screenshots import ScreenshotsDialog from bauh.view.qt.styles import StylesComboBox from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \ AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots from bauh.view.qt.view_model import PackageView from bauh.view.qt.view_utils import load_icon +from bauh.view.util import util, resource DARK_ORANGE = '#FF4500' @@ -283,6 +283,7 @@ class ManageWindow(QWidget): self._maximized = False self.progress_controll_enabled = True self.recent_installation = False + self.recent_uninstall = False self.dialog_about = None self.first_refresh = suggestions @@ -437,7 +438,7 @@ class ManageWindow(QWidget): def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None): self.recent_installation = False - self.type_filter = None + # self.type_filter = None self.input_search.clear() if not keep_console: @@ -445,7 +446,7 @@ class ManageWindow(QWidget): self.ref_checkbox_updates.setVisible(False) self.ref_checkbox_only_apps.setVisible(False) - self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=True) + self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=not self.recent_uninstall) self.thread_refresh.app = top_app # the app will be on top when refresh happens self.thread_refresh.pkg_types = pkg_types @@ -455,8 +456,9 @@ class ManageWindow(QWidget): self.finish_action() self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) self.ref_bt_upgrade.setVisible(True) - self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types']) + self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall) self.first_refresh = False + self.recent_uninstall = False self._hide_fields_after_recent_installation() def uninstall_app(self, app: PackageView): @@ -470,7 +472,7 @@ class ManageWindow(QWidget): return self._handle_console_option(True) - self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name)) + self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name), clear_filters=False) self.thread_uninstall.app = app self.thread_uninstall.root_password = pwd @@ -489,6 +491,7 @@ class ManageWindow(QWidget): util.notify_user('{} ({}) {}'.format(pkgv.model.name, pkgv.model.get_type(), self.i18n['uninstalled'])) only_pkg_type = len([p for p in self.pkgs if p.model.get_type() == pkgv.model.get_type()]) >= 2 + self.recent_uninstall = True self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None) else: if self._can_notify_user(): @@ -553,7 +556,7 @@ class ManageWindow(QWidget): self.ref_bt_upgrade.setVisible(show_bt_upgrade) - def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True): + def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False): self.update_bt_upgrade(pkgs_info) if pkgs_info['updates'] > 0: @@ -562,13 +565,14 @@ class ManageWindow(QWidget): if not self.ref_checkbox_updates.isVisible(): self.ref_checkbox_updates.setVisible(True) - if not self.filter_updates: + if not self.filter_updates and not keep_selected: self._change_checkbox(self.checkbox_updates, True, 'filter_updates', trigger_filters) - if pkgs_info['napp_updates'] > 0 and self.filter_only_apps: + if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected: self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters) else: - self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters) + if not keep_selected: + self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters) self.ref_checkbox_updates.setVisible(False) @@ -592,7 +596,7 @@ class ManageWindow(QWidget): 'display_limit': self.display_limit if updates <= 0 else None } - def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False): + def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False): self.input_name_filter.setText('') pkgs_info = commons.new_pkgs_info() filters = self._gen_filters(ignore_updates) @@ -620,21 +624,24 @@ class ManageWindow(QWidget): commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) - if pkgs_info['apps_count'] == 0: - if self.first_refresh: - self._begin_search('') - self.thread_suggestions.start() - return + if not keep_filters: + if pkgs_info['apps_count'] == 0: + if self.first_refresh: + self._begin_search('') + self.thread_suggestions.start() + return + else: + self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) + self.checkbox_only_apps.setCheckable(False) else: - self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) - self.checkbox_only_apps.setCheckable(False) - else: - self.checkbox_only_apps.setCheckable(True) - self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) + self.checkbox_only_apps.setCheckable(True) + self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(self.pkgs)) + self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(self.pkgs)) + self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(self.pkgs)) self._apply_filters(pkgs_info, ignore_updates=ignore_updates) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(self.pkgs)) self.pkgs_available = pkgs_info['pkgs'] @@ -646,9 +653,6 @@ class ManageWindow(QWidget): if self.pkgs: self.ref_input_name_filter.setVisible(True) - self._update_type_filters(pkgs_info['available_types']) - self._update_categories(pkgs_info['categories']) - self._update_table(pkgs_info=pkgs_info) if new_pkgs: @@ -666,19 +670,25 @@ class ManageWindow(QWidget): for pkgv in pkgs_info['pkgs']: commons.apply_filters(pkgv, filters, pkgs_info) - def _update_type_filters(self, available_types: dict = None): + def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False): if available_types is None: self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 1) else: - self.type_filter = self.any_type_filter + keeping_selected = keep_selected and available_types and self.type_filter in available_types + + if not keeping_selected: + self.type_filter = self.any_type_filter if available_types and len(available_types) > 1: if self.combo_filter_type.count() > 1: for _ in range(self.combo_filter_type.count() - 1): self.combo_filter_type.removeItem(1) - for app_type, icon_path in available_types.items(): + sel_type = -1 + for idx, item in enumerate(available_types.items()): + app_type, icon_path = item[0], item[1] + icon = self.cache_type_filter_icons.get(app_type) if not icon: @@ -687,28 +697,45 @@ class ManageWindow(QWidget): self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type) + if keeping_selected and app_type == self.type_filter: + sel_type = idx + 1 + + self.combo_filter_type.blockSignals(True) + self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0) + self.combo_filter_type.blockSignals(False) self.ref_combo_filter_type.setVisible(True) else: self.ref_combo_filter_type.setVisible(False) - def _update_categories(self, categories: Set[str] = None): + def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False): if categories is None: self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) else: - self.category_filter = self.any_category_filter + keeping_selected = keep_selected and categories and self.category_filter in categories + + if not keeping_selected: + self.category_filter = self.any_category_filter if categories: if self.combo_categories.count() > 1: for _ in range(self.combo_categories.count() - 1): self.combo_categories.removeItem(1) - for c in categories: + selected_cat = -1 + for idx, c in enumerate(categories): i18n_cat = self.i18n.get(c) cat_label = i18n_cat if i18n_cat else c self.combo_categories.addItem(cat_label.capitalize(), c) + if keeping_selected and c == self.category_filter: + selected_cat = idx + 1 + + self.combo_categories.blockSignals(True) + self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0) + self.combo_categories.blockSignals(False) self.ref_combo_categories.setVisible(True) + else: self.ref_combo_categories.setVisible(False) @@ -815,7 +842,7 @@ class ManageWindow(QWidget): else: self.combo_filter_type.setEnabled(False) - def finish_action(self): + def finish_action(self, keep_filters: bool = False): self.ref_combo_styles.setVisible(True) self.thread_animate_progress.stop = True self.thread_animate_progress.wait(msecs=1000) @@ -841,8 +868,8 @@ class ManageWindow(QWidget): if self.pkgs: self.ref_input_name_filter.setVisible(True) self.update_bt_upgrade() - self._update_type_filters() - self._update_categories() + self._update_type_filters(keep_selected=keep_filters) + self._update_categories(keep_selected=keep_filters) if self.ref_bt_installed.isVisible(): self.ref_bt_installed.setEnabled(True) From 0494e9c6d76593c79b0cd92381982b0273e0a4b4 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 18:19:50 -0300 Subject: [PATCH 29/46] fix: not generating clean filters of all aplications of a given type were uninstalled --- bauh/view/qt/window.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index a243cdc8..8361a6e8 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -456,7 +456,7 @@ class ManageWindow(QWidget): self.finish_action() self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) self.ref_bt_upgrade.setVisible(True) - self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall) + self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall and res['types']) self.first_refresh = False self.recent_uninstall = False self._hide_fields_after_recent_installation() @@ -624,24 +624,25 @@ class ManageWindow(QWidget): commons.update_info(pkgv, pkgs_info) commons.apply_filters(pkgv, filters, pkgs_info) - if not keep_filters: - if pkgs_info['apps_count'] == 0: - if self.first_refresh: - self._begin_search('') - self.thread_suggestions.start() - return - else: + if pkgs_info['apps_count'] == 0: + if self.first_refresh: + self._begin_search('') + self.thread_suggestions.start() + return + else: + if not keep_filters: self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) self.checkbox_only_apps.setCheckable(False) - else: + else: + if not keep_filters: self.checkbox_only_apps.setCheckable(True) self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(self.pkgs)) - self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(self.pkgs)) - self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(self.pkgs)) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) + self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self._apply_filters(pkgs_info, ignore_updates=ignore_updates) - self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(self.pkgs)) + self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self.pkgs_available = pkgs_info['pkgs'] From eaa98d8a71b609f4f1bbda891d6ac6e493b7062d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 23 Oct 2019 19:06:12 -0300 Subject: [PATCH 30/46] [aur] fix: encoding dependency name before requesting info from the API --- bauh/gems/arch/aur.py | 7 +++---- bauh/view/resources/locale/es | 1 + bauh/view/resources/locale/pt | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index ce01364f..857c4be2 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -1,9 +1,8 @@ import re from typing import Set, List -import requests - from bauh.api.http import HttpClient +import urllib.parse URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&' URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=' @@ -32,7 +31,7 @@ class AURClient: return res['results'] if res and res.get('results') else [] def get_src_info(self, name: str) -> dict: - res = self.http_client.get(URL_SRC_INFO + name) + res = self.http_client.get(URL_SRC_INFO + urllib.parse.quote(name)) if res and res.text: info = {} @@ -48,4 +47,4 @@ class AURClient: return info def _map_names_as_queries(self, names) -> str: - return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)]) + return '&'.join(['arg[{}]={}'.format(i, urllib.parse.quote(n)) for i, n in enumerate(names)]) diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index 78237bcb..dbb9f951 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -183,6 +183,7 @@ packagemanager=administrador de paquetes sportsgame=juego de deportes arcadegame=juego arcade player=jugador +fonts=fuentes screenshots.bt_next.label=próxima screenshots.bt_back.label=anterior screenshots,download.running=descargando imagen diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 045d61c7..eeb3c40e 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -186,6 +186,7 @@ packagemanager=gerenciador de pacotes sportsgame=jogo de esportes arcadegame=jogo arcade player=jogador +fonts=fontes screenshots.bt_next.label=próxima screenshots.bt_back.label=anterior screenshots,download.running=baixando imagem From fc810c6b9ae73a9dd2ba5228ad59f1e0ddbbbd9d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 11:01:42 -0300 Subject: [PATCH 31/46] [categories] not updating categories filter --- bauh/view/qt/window.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 8361a6e8..22eca757 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -407,6 +407,20 @@ class ManageWindow(QWidget): def _notify_model_data_change(self): self.table_apps.fill_async_data() + if not self.recent_installation: + self._reload_categories() + + def _reload_categories(self): + + categories = set() + + for p in self.pkgs_available: + if p.model.categories: + categories.update(p.model.categories) + + if categories: + self._update_categories(categories, keep_selected=True) + def changeEvent(self, e: QEvent): if isinstance(e, QWindowStateChangeEvent): self._maximized = self.isMaximized() @@ -438,7 +452,6 @@ class ManageWindow(QWidget): def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None): self.recent_installation = False - # self.type_filter = None self.input_search.clear() if not keep_console: @@ -725,11 +738,12 @@ class ManageWindow(QWidget): selected_cat = -1 for idx, c in enumerate(categories): - i18n_cat = self.i18n.get(c) + lower_cat = c.lower() + i18n_cat = self.i18n.get(lower_cat) cat_label = i18n_cat if i18n_cat else c - self.combo_categories.addItem(cat_label.capitalize(), c) + self.combo_categories.addItem(cat_label.capitalize(), lower_cat) - if keeping_selected and c == self.category_filter: + if keeping_selected and lower_cat == self.category_filter: selected_cat = idx + 1 self.combo_categories.blockSignals(True) From 29d60ad56c4aff881eaf5b30e34ae8d4a76797fa Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 12:33:42 -0300 Subject: [PATCH 32/46] [snap] snaps not treated as applications with be categorized as runtime at least --- CHANGELOG.md | 4 +++- bauh/gems/snap/controller.py | 18 ++++++++++++++++-- bauh/gems/snap/model.py | 3 ++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 148f8379..8c023e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AppImage: - showing an error popup when **AppImageLauncher** messes up with an application installation - Flatpak: - - Runtimes now are categorized as "runtimes" + - Runtimes now are categorized as "runtime" - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) - showing a warning popup when the Snap API is out + - Snaps not treated as applications with be categorized as "runtime" at least - minor thread improvements ### UI @@ -33,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - application not initializing when there is no internet connection - not loading application icons after some filters are applied to the table results +- not reloading the available categories after asynchronous data is fetched - AUR: - update-checking for some scenarios - not respecting **ignorepkg** settings in **pacman.conf** diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 063f5575..25054bd2 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -47,13 +47,27 @@ class SnapManager(SoftwareManager): latest_version=app_json.get('version'), description=app_json.get('description', app_json.get('summary'))) - if app.publisher: + if app.publisher and app.publisher.endswith('*'): + app.verified_publisher = True app.publisher = app.publisher.replace('*', '') - app.categories = self.categories.get(app.name.lower()) + categories = self.categories.get(app.name.lower()) + + if categories: + app.categories = categories app.installed = installed + if not app.is_application(): + categories = app.categories + + if categories is None: + categories = [] + app.categories = categories + + if 'runtime' not in categories: + categories.append('runtime') + api_data = self.api_cache.get(app_json['name']) expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow() diff --git a/bauh/gems/snap/model.py b/bauh/gems/snap/model.py index 711d67e4..6a48928d 100644 --- a/bauh/gems/snap/model.py +++ b/bauh/gems/snap/model.py @@ -19,7 +19,7 @@ class SnapApplication(SoftwarePackage): def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None, publisher: str = None, rev: str = None, notes: str = None, - confinement: str = None, has_apps_field: bool = None): + confinement: str = None, has_apps_field: bool = None, verified_publisher: bool = False): super(SnapApplication, self).__init__(id=id, name=name, version=version, latest_version=latest_version, description=description) self.publisher = publisher @@ -27,6 +27,7 @@ class SnapApplication(SoftwarePackage): self.notes = notes self.confinement = confinement self.has_apps_field = has_apps_field + self.verified_publisher = verified_publisher def supports_disk_cache(self): return self.installed From 0a2a3175697426d495158ce8b0e9583558886dfc Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 12:49:34 -0300 Subject: [PATCH 33/46] [aur] version suffix checking only for 'bauh' and 'bauh-staging' --- bauh/gems/arch/mapper.py | 39 +++--- tests/gems/arch/arch_data_mapper_test.py | 168 +++++++++++------------ 2 files changed, 105 insertions(+), 102 deletions(-) diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index 723c01ee..55e05d16 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -9,6 +9,7 @@ URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}' RE_LETTERS = re.compile(r'\.([a-zA-Z]+)-\d+$') RE_VERSION_SPLIT = re.compile(r'[a-zA-Z]+|\d+|[\.\-_@#]+') +BAUH_PACKAGES = {'bauh', 'bauh-staging'} RE_SFX = ('r', 're', 'release') GA_SFX = ('ga', 'ge') RC_SFX = ('rc',) @@ -48,32 +49,34 @@ class ArchDataMapper: pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None - pkg.update = self.check_update(pkg.version, pkg.latest_version) + pkg.update = self.check_update(pkg.version, pkg.latest_version, check_suffix=pkg.name in BAUH_PACKAGES) @staticmethod - def check_update(version: str, latest_version: str) -> bool: + def check_update(version: str, latest_version: str, check_suffix: bool = False) -> bool: if version and latest_version: - current_sfx = RE_LETTERS.findall(version) - latest_sf = RE_LETTERS.findall(latest_version) - if latest_sf and current_sfx: - current_sfx = current_sfx[0] - latest_sf = latest_sf[0] + if check_suffix: + current_sfx = RE_LETTERS.findall(version) + latest_sf = RE_LETTERS.findall(latest_version) - current_sfx_data = V_SUFFIX_MAP.get(current_sfx.lower()) - latest_sfx_data = V_SUFFIX_MAP.get(latest_sf.lower()) + if latest_sf and current_sfx: + current_sfx = current_sfx[0] + latest_sf = latest_sf[0] - if current_sfx_data and latest_sfx_data: - nversion = version.split(current_sfx)[0] - nlatest = latest_version.split(latest_sf)[0] + current_sfx_data = V_SUFFIX_MAP.get(current_sfx.lower()) + latest_sfx_data = V_SUFFIX_MAP.get(latest_sf.lower()) - if nversion == nlatest: - if current_sfx_data['c'] != latest_sfx_data['c']: - return latest_sfx_data['p'] < current_sfx_data['p'] - else: - return ''.join(latest_version.split(latest_sf)) > ''.join(version.split(current_sfx)) + if current_sfx_data and latest_sfx_data: + nversion = version.split(current_sfx)[0] + nlatest = latest_version.split(latest_sf)[0] - return nlatest > nversion + if nversion == nlatest: + if current_sfx_data['c'] != latest_sfx_data['c']: + return latest_sfx_data['p'] < current_sfx_data['p'] + else: + return ''.join(latest_version.split(latest_sf)) > ''.join(version.split(current_sfx)) + + return nlatest > nversion latest_split = RE_VERSION_SPLIT.findall(latest_version) current_split = RE_VERSION_SPLIT.findall(version) diff --git a/tests/gems/arch/arch_data_mapper_test.py b/tests/gems/arch/arch_data_mapper_test.py index 6dfb42c8..fdc17d5a 100644 --- a/tests/gems/arch/arch_data_mapper_test.py +++ b/tests/gems/arch/arch_data_mapper_test.py @@ -38,97 +38,97 @@ class ArchDataMapperTest(TestCase): def test_check_update_release(self): # RE - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-2', '1.0.0.R-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.R-5')) - self.assertTrue(ArchDataMapper.check_update('1.0.1.R-1', '1.0.1.R-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.2.R-1', '1.0.1.R-7')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-2', '1.0.0.R-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.R-5', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.1.R-1', '1.0.1.R-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.2.R-1', '1.0.1.R-7', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.R-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RELEASE-2', '1.0.0.R-5')) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RELEASE-5')) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.R-2', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.RELEASE-2', '1.0.0.R-5', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RELEASE-5', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1', check_suffix=True)) # GA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.Ge-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GE-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GE-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GE-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GA-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GA-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.Ge-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GE-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GE-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GE-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.GA-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.GA-1', check_suffix=True)) # RCS - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.RC-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.RC-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.RC-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.RC-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.RC-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.RC-1', check_suffix=True)) # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.B-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.B-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.BETA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.BETA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.BETA-2')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.B-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.B-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.BETA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.BETA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.BETA-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.BETA-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.BETA-1', check_suffix=True)) # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALPHA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALFA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALFA-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALFA-2')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALFA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALFA-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALFA-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.Alfa-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.Alfa-1', check_suffix=True)) # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DeV-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEVEL-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DEV-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEV-2')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DeV-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEVEL-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DEV-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEV-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.DEV-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.DEV-1', check_suffix=True)) def test_check_update_ga(self): # GA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.GA-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-2', '1.0.1.GA-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.GA-2', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-2', '1.0.1.GA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10', check_suffix=True)) # RC - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.RC-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.RC-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.10.GA-10', '1.1.0.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1')) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.RC-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.10.GA-10', '1.1.0.RC-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1', check_suffix=True)) # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.BETA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.BETA-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1', check_suffix=True)) # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.ALPHA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.ALPHA-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1', check_suffix=True)) # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.DEV-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.DEV-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1', check_suffix=True)) def test_check_update_rc(self): # RC @@ -151,38 +151,38 @@ class ArchDataMapperTest(TestCase): def test_check_update_beta(self): # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-2', '1.0.1.B-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-2', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-2', '1.0.1.B-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10', check_suffix=True)) # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.ALPHA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.ALPHA-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1', check_suffix=True)) # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.DEV-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.DEV-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1', check_suffix=True)) def test_check_update_alpha(self): # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALPHA-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.ALPHA-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-2', '1.0.1.ALFA-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALPHA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.ALPHA-2', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-2', '1.0.1.ALFA-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10', check_suffix=True)) # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.DEV-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.DEV-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1', check_suffix=True)) def test_check_update_dev(self): - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEV-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEVEL-1', '1.0.0.DEVELOPMENT-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.DEVEL-2', '1.0.1.DEV-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.DEV-3', '1.0.6.DEVELOPMENT-10')) + self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEV-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.0.0.DEVEL-1', '1.0.0.DEVELOPMENT-1', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-2', check_suffix=True)) + self.assertTrue(ArchDataMapper.check_update('1.0.0.DEVEL-2', '1.0.1.DEV-1', check_suffix=True)) + self.assertFalse(ArchDataMapper.check_update('1.1.0.DEV-3', '1.0.6.DEVELOPMENT-10', check_suffix=True)) def test_check_update_unknown_suffix(self): self.assertTrue(ArchDataMapper.check_update('1.0.0.BALL-1', '1.0.0.TAR-1')) From 7c5a0c80ae378565ee5ed46f26fa5778a0f1b939 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 13:02:07 -0300 Subject: [PATCH 34/46] [aur] mapping known search key words to the specific package name --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c023e7d..59768601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) + - mapping known search key words to the specific package name ( e.g:"google chrome" will become "google-chrome" ) - Snap: - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) - caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off ) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index e3f811be..6bc68437 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -35,6 +35,12 @@ RE_SPLIT_VERSION = re.compile(r'(=|>|<)') SOURCE_FIELDS = ('source', 'source_x86_64') RE_PRE_DOWNLOADABLE_FILES = re.compile(r'(https?|ftp)://.+\.\w+[^gpg|git]$') +SEARCH_OPTIMIZED_MAP = { + 'google chrome': 'google-chrome', + 'chrome google': 'google-chrome', + 'googlechrome': 'google-chrome' +} + class ArchManager(SoftwareManager): @@ -81,7 +87,9 @@ class ArchManager(SoftwareManager): read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()), daemon=True) read_installed.start() - api_res = self.aur_client.search(words) + mapped_words = SEARCH_OPTIMIZED_MAP.get(words) + + api_res = self.aur_client.search(mapped_words if mapped_words else words) if api_res and api_res.get('results'): read_installed.join() From 97fe290bffeef776bfb9171c065634dda926323e Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 13:18:07 -0300 Subject: [PATCH 35/46] [aur] i18n --- bauh/gems/arch/resources/locale/en | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 47e1cc3b..31816086 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -62,4 +62,4 @@ arch.install.aur.unknown_key.body=To continue {} installation is necessary to tr arch.aur.install.unknown_key.status=Receiving public key {} arch.aur.install.unknown_key.receive_error=Could not receive public key {} arch.aur.install.validity_check.title=Integrity issues -arch.aur.install.validity_check.body=Some of the source-files needed for {} installation are not healthy. The installation will be cancelled to avoid damages to your system. \ No newline at end of file +arch.aur.install.validity_check.body=Some of the source-files needed for {} installation are not healthy. The installation will be cancelled to prevent damage to your system. \ No newline at end of file From c323ed125b181c1bdf98159e3fa4b1d954e14de2 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 14:52:25 -0300 Subject: [PATCH 36/46] [snap] fix: not showing 'API is out' popup when there is no internet connection --- bauh/api/abstract/controller.py | 3 ++- bauh/gems/appimage/controller.py | 2 +- bauh/gems/arch/controller.py | 2 +- bauh/gems/flatpak/controller.py | 2 +- bauh/gems/snap/controller.py | 11 ++++++----- bauh/view/core/controller.py | 6 ++++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 2a00373a..41d9abd1 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -213,8 +213,9 @@ class SoftwareManager(ABC): pass @abstractmethod - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool) -> List[str]: """ + :param internet_available :return: a list of warnings to be shown to the user """ pass diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 1d910b65..1f5ab7f9 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -396,7 +396,7 @@ class AppImageManager(SoftwareManager): return updates - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool) -> List[str]: pass def list_suggestions(self, limit: int) -> List[PackageSuggestion]: diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 6bc68437..62e826f5 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -710,7 +710,7 @@ class ArchManager(SoftwareManager): installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed return [PackageUpdate(p.id, p.latest_version, 'aur') for p in installed if p.update] - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool) -> List[str]: warnings = [] if self.arch_distro: diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 43445cb4..4e12c0d1 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -248,7 +248,7 @@ class FlatpakManager(SoftwareManager): return updates - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool) -> List[str]: if flatpak.is_installed(): if not flatpak.has_remotes_set(): return [self.i18n['flatpak.notification.no_remotes']] diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 25054bd2..7ce13691 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -196,17 +196,18 @@ class SnapManager(SoftwareManager): def list_updates(self, internet_available: bool) -> List[PackageUpdate]: pass - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool) -> List[str]: if snap.is_installed() and not snap.is_snapd_running(): snap_bold = bold('Snap') return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), self.i18n['snap.notification.snap.disable'].format(snap_bold, bold(self.i18n['manage_window.settings.gems']))] - available, output = snap.is_api_available() + if internet_available: + available, output = snap.is_api_available() - if not available: - self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) - return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] + if not available: + self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) + return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority, out: List[PackageSuggestion]): res = self.http_client.get_json(SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name)) diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 9769c109..e0f65f4a 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -307,13 +307,15 @@ class GenericSoftwareManager(SoftwareManager): return updates - def list_warnings(self) -> List[str]: + def list_warnings(self, internet_available: bool = None) -> List[str]: warnings = [] if self.managers: + int_available = internet.is_available(self.context.http_client, self.context.logger) + for man in self.managers: if man.is_enabled(): - man_warnings = man.list_warnings() + man_warnings = man.list_warnings(internet_available=int_available) if man_warnings: if warnings is None: From 8d7cf689bdb34ebaa8d5367b7cd860a8eaa90d67 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 15:10:28 -0300 Subject: [PATCH 37/46] forcing table redraw when filters are applied --- bauh/view/qt/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 22eca757..fdc963b4 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -316,6 +316,7 @@ class ManageWindow(QWidget): self.update_bt_upgrade(pkgs_info) if self.pkgs_available: + self._notify_model_data_change() self.thread_verify_models.work = False self.thread_verify_models.wait(50) self.thread_verify_models.apps = self.pkgs_available From e0041a3c738298cc5bb65fb3ecab9751ee7d0c5f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 15:51:05 -0300 Subject: [PATCH 38/46] [flatpak] fix: handling json decode error when retrieving screenshots --- bauh/gems/flatpak/controller.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 4e12c0d1..e87ef831 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -290,7 +290,10 @@ class FlatpakManager(SoftwareManager): if s.get('imgDesktopUrl'): urls.append(s['imgDesktopUrl']) - except json.decoder.JSONDecodeError: - self.context.logger.error("Could not decode json from '{}'".format(screenshots_url)) + except Exception as e: + if e.__class__.__name__ == 'JSONDecodeError': + self.context.logger.error("Could not decode json from '{}'".format(screenshots_url)) + else: + traceback.print_exc() return urls From 9a412c9981a38ba2f087e98eb13cf68b6d7c660d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 16:50:26 -0300 Subject: [PATCH 39/46] [about] version font size --- bauh/view/qt/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/view/qt/about.py b/bauh/view/qt/about.py index 9f730fdd..7f3971a3 100644 --- a/bauh/view/qt/about.py +++ b/bauh/view/qt/about.py @@ -56,7 +56,7 @@ class AboutDialog(QDialog): layout.addWidget(QLabel('')) label_version = QLabel(locale_keys['version'].lower() + ' ' + __version__) - label_version.setStyleSheet('QLabel { font-size: 10px; font-weight: bold }') + label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }') label_version.setAlignment(Qt.AlignCenter) layout.addWidget(label_version) From 064ef53b8e78271835249bff6e85ab67d070211e Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 17:12:09 -0300 Subject: [PATCH 40/46] [flatpak] not keeping categories associated with flatpaks selected --- bauh/view/qt/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index fdc963b4..f5fe6bee 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -727,7 +727,7 @@ class ManageWindow(QWidget): if categories is None: self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) else: - keeping_selected = keep_selected and categories and self.category_filter in categories + keeping_selected = keep_selected and categories and self.category_filter in {c.lower() for c in categories} if not keeping_selected: self.category_filter = self.any_category_filter From 1a4ccfcefe6cbc38bd31d1ef341a17125c2bd23d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 17:37:42 -0300 Subject: [PATCH 41/46] [flatpak] Formatting the API categories to the same format provided by the other packaging technologies --- CHANGELOG.md | 1 + bauh/gems/flatpak/controller.py | 4 +++- bauh/gems/flatpak/worker.py | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59768601..5fba51e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - showing an error popup when **AppImageLauncher** messes up with an application installation - Flatpak: - Runtimes now are categorized as "runtime" + - Formatting the API categories to the same format provided by the other packaging technologies - AUR: - showing a "user-friendly" popup when there are integrity issues with the source-files of a building package - not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time ) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index e87ef831..0da44b1c 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -25,6 +25,7 @@ class FlatpakManager(SoftwareManager): super(FlatpakManager, self).__init__(context=context) self.i18n = context.i18n self.api_cache = context.cache_factory.new() + self.category_cache = context.cache_factory.new() context.disk_loader_factory.map(FlatpakApplication, self.api_cache) self.enabled = True self.http_client = context.http_client @@ -46,7 +47,8 @@ class FlatpakManager(SoftwareManager): disk_loader.fill(app) # preloading cached disk data if internet: - FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start() + FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, + context=self.context, category_cache=self.category_cache).start() else: app.fill_cached_data(api_data) diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py index dcb5e933..cbc9812e 100644 --- a/bauh/gems/flatpak/worker.py +++ b/bauh/gems/flatpak/worker.py @@ -1,3 +1,4 @@ +import re import traceback from threading import Thread @@ -9,10 +10,12 @@ from bauh.api.http import HttpClient from bauh.gems.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL from bauh.gems.flatpak.model import FlatpakApplication +RE_SPLIT_UPPER = re.compile(r'[A-Z][a-z]*') + class FlatpakAsyncDataLoader(Thread): - def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache): + def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache, category_cache: MemoryCache): super(FlatpakAsyncDataLoader, self).__init__(daemon=True) self.app = app self.manager = manager @@ -20,6 +23,7 @@ class FlatpakAsyncDataLoader(Thread): self.api_cache = api_cache self.persist = False self.logger = context.logger + self.category_cache = category_cache def run(self): if self.app: @@ -51,7 +55,17 @@ class FlatpakAsyncDataLoader(Thread): self.app.icon_url = FLATHUB_URL + self.app.icon_url if data.get('categories'): - self.app.categories = [c['name'] for c in data['categories']] + cats = [] + for c in data['categories']: + cached = self.category_cache.get(c['name']) + + if not cached: + cached = ' '.join(RE_SPLIT_UPPER.findall(c['name'])).lower() + self.category_cache.add_non_existing(c['name'], cached) + + cats.append(cached) + + self.app.categories = cats loaded_data = self.app.get_data_to_cache() From c631ecdb0667911f0f083eace12ae7c6b6488704 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 18:18:47 -0300 Subject: [PATCH 42/46] fix: not keeping the update toggle-button state after a filter is applied --- CHANGELOG.md | 2 ++ bauh/view/qt/apps_table.py | 2 +- bauh/view/qt/thread.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fba51e9..acd24e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Improvements - 3 password attempts for root authentication - not changing the table applied filters after a uninstall +- cleaning the progress bar substatus after each upgrade - AppImage: - showing an error popup when **AppImageLauncher** messes up with an application installation - Flatpak: @@ -37,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - application not initializing when there is no internet connection - not loading application icons after some filters are applied to the table results - not reloading the available categories after asynchronous data is fetched +- not keeping the update toggle-button state after a filter is applied - AUR: - update-checking for some scenarios - not respecting **ignorepkg** settings in **pacman.conf** diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 9fb31327..c4c39a4a 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -231,7 +231,7 @@ class AppsTable(QTableWidget): col_update = None if update_check_enabled and pkg.model.update: - col_update = UpdateToggleButton(pkg, self.window, self.i18n, pkg.model.update) + col_update = UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked) self.setCellWidget(pkg.table_index, 7, col_update) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 2f96d799..f3be47e0 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -64,8 +64,7 @@ class AsyncAction(QThread, ProcessWatcher): self.signal_status.emit(status) def change_substatus(self, substatus: str): - if substatus: - self.signal_substatus.emit(substatus) + self.signal_substatus.emit(substatus) def change_progress(self, val: int): if val is not None: @@ -96,6 +95,7 @@ class UpdateSelectedApps(AsyncAction): self.change_status('{} {} {}...'.format(self.locale_keys['manage_window.status.upgrading'], name, app.model.version)) success = bool(self.manager.update(app.model, self.root_password, self)) + self.change_substatus('') if not success: break From 669206e873d75dd98633bfd0be4c2f3452c24272 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 24 Oct 2019 19:21:58 -0300 Subject: [PATCH 43/46] [flatpak] fix: upper categories split --- bauh/gems/flatpak/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py index cbc9812e..cdf162ed 100644 --- a/bauh/gems/flatpak/worker.py +++ b/bauh/gems/flatpak/worker.py @@ -60,7 +60,7 @@ class FlatpakAsyncDataLoader(Thread): cached = self.category_cache.get(c['name']) if not cached: - cached = ' '.join(RE_SPLIT_UPPER.findall(c['name'])).lower() + cached = ' '.join(RE_SPLIT_UPPER.findall(c['name'])).lower() if not c['name'].isupper() else c['name'].lower() self.category_cache.add_non_existing(c['name'], cached) cats.append(cached) From 16586a7d2886876c5bdd697dbf5203a2425e50ea Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 25 Oct 2019 11:22:23 -0300 Subject: [PATCH 44/46] [flatpak] fix: category formatting | sorting categories --- CHANGELOG.md | 1 + bauh/gems/flatpak/worker.py | 20 ++++++++++++++++---- bauh/view/qt/commons.py | 3 ++- bauh/view/qt/window.py | 19 +++++++++++-------- tests/gems/flatpak/__init__.py | 0 tests/gems/flatpak/worker_test.py | 20 ++++++++++++++++++++ 6 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 tests/gems/flatpak/__init__.py create mode 100644 tests/gems/flatpak/worker_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index acd24e20..7f72eb08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 3 password attempts for root authentication - not changing the table applied filters after a uninstall - cleaning the progress bar substatus after each upgrade +- sorted categories - AppImage: - showing an error popup when **AppImageLauncher** messes up with an application installation - Flatpak: diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py index cdf162ed..5f581146 100644 --- a/bauh/gems/flatpak/worker.py +++ b/bauh/gems/flatpak/worker.py @@ -1,5 +1,5 @@ -import re import traceback +from io import StringIO from threading import Thread from bauh.api.abstract.cache import MemoryCache @@ -10,8 +10,6 @@ from bauh.api.http import HttpClient from bauh.gems.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL from bauh.gems.flatpak.model import FlatpakApplication -RE_SPLIT_UPPER = re.compile(r'[A-Z][a-z]*') - class FlatpakAsyncDataLoader(Thread): @@ -25,6 +23,20 @@ class FlatpakAsyncDataLoader(Thread): self.logger = context.logger self.category_cache = category_cache + @staticmethod + def format_category(category: str) -> str: + word = StringIO() + last_l = None + for idx, l in enumerate(category): + if idx != 0 and last_l != ' ' and l.isupper() and idx + 1 < len(category) and category[idx + 1].islower(): + word.write(' ') + + last_l = l.lower() + word.write(last_l) + + word.seek(0) + return word.read() + def run(self): if self.app: self.app.status = PackageStatus.LOADING_DATA @@ -60,7 +72,7 @@ class FlatpakAsyncDataLoader(Thread): cached = self.category_cache.get(c['name']) if not cached: - cached = ' '.join(RE_SPLIT_UPPER.findall(c['name'])).lower() if not c['name'].isupper() else c['name'].lower() + cached = self.format_category(c['name']) self.category_cache.add_non_existing(c['name'], cached) cats.append(cached) diff --git a/bauh/view/qt/commons.py b/bauh/view/qt/commons.py index 6ba6f90b..4d652109 100644 --- a/bauh/view/qt/commons.py +++ b/bauh/view/qt/commons.py @@ -31,7 +31,8 @@ def update_info(pkgv: PackageView, pkgs_info: dict): pkgs_info['updates'] += 1 if pkgv.model.categories: - pkgs_info['categories'].update([s.lower() for s in pkgv.model.categories]) + for c in pkgv.model.categories: + pkgs_info['categories'].add(c.lower()) pkgs_info['pkgs'].append(pkgv) pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0 diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index f5fe6bee..a4272147 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -417,7 +417,8 @@ class ManageWindow(QWidget): for p in self.pkgs_available: if p.model.categories: - categories.update(p.model.categories) + for c in p.model.categories: + categories.add(c.lower()) if categories: self._update_categories(categories, keep_selected=True) @@ -727,7 +728,7 @@ class ManageWindow(QWidget): if categories is None: self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) else: - keeping_selected = keep_selected and categories and self.category_filter in {c.lower() for c in categories} + keeping_selected = keep_selected and categories and self.category_filter in categories if not keeping_selected: self.category_filter = self.any_category_filter @@ -738,13 +739,15 @@ class ManageWindow(QWidget): self.combo_categories.removeItem(1) selected_cat = -1 - for idx, c in enumerate(categories): - lower_cat = c.lower() - i18n_cat = self.i18n.get(lower_cat) - cat_label = i18n_cat if i18n_cat else c - self.combo_categories.addItem(cat_label.capitalize(), lower_cat) + cat_list = list(categories) + cat_list.sort() - if keeping_selected and lower_cat == self.category_filter: + for idx, c in enumerate(cat_list): + i18n_cat = self.i18n.get(c) + cat_label = i18n_cat if i18n_cat else c + self.combo_categories.addItem(cat_label.capitalize(), c) + + if keeping_selected and c == self.category_filter: selected_cat = idx + 1 self.combo_categories.blockSignals(True) diff --git a/tests/gems/flatpak/__init__.py b/tests/gems/flatpak/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/gems/flatpak/worker_test.py b/tests/gems/flatpak/worker_test.py new file mode 100644 index 00000000..cdc7886d --- /dev/null +++ b/tests/gems/flatpak/worker_test.py @@ -0,0 +1,20 @@ +from unittest import TestCase + +from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader + + +class FlatpakAsyncDataLoaderTest(TestCase): + + def test_format_category(self): + self.assertEqual('irc client', FlatpakAsyncDataLoader.format_category('IRCClient')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('TextEditor')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('Text Editor')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('text editor')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('Text editor')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('text Editor')) + self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('textEditor')) + self.assertEqual('ide', FlatpakAsyncDataLoader.format_category('IDE')) + self.assertEqual('faster irc client', FlatpakAsyncDataLoader.format_category('Faster IRCClient')) + self.assertEqual('3d graphics', FlatpakAsyncDataLoader.format_category('3DGraphics')) + self.assertEqual('32d graphics', FlatpakAsyncDataLoader.format_category('32DGraphics')) + self.assertEqual('d32 graphics', FlatpakAsyncDataLoader.format_category('D32Graphics')) From c5df1bd68632d588b3467fe20fbe5111cbd612ff Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 25 Oct 2019 14:16:03 -0300 Subject: [PATCH 45/46] [CHANGELOG] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f72eb08..266c6266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [0.7.1] 2019- +## [0.7.1] 2019-10-25 ### Features - Snap: - if the **stable** channel is not available while an application is being installed, a popup is displayed allowing the user to choose a different one ( e.g: dbeaver-ce ) From dda3082a03d6c3246783cca610c50defeb8ca5f6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 25 Oct 2019 21:10:42 -0300 Subject: [PATCH 46/46] [MANIFEST.in] including tests --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index e2d034a9..7df4aa32 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include LICENSE CHANGELOG.md README.md include requirements.txt recursive-include bauh/view/resources * recursive-include bauh/gems/*/resources * +recursive-include tests *