0.7.1
45
CHANGELOG.md
@@ -4,6 +4,49 @@ 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-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 )
|
||||
|
||||
### Improvements
|
||||
- 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:
|
||||
- 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 )
|
||||
- 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 )
|
||||
- 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
|
||||
- Screenshots panel:
|
||||
- "downloading" label replaced by a progress bar
|
||||
|
||||
### 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
|
||||
- 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**
|
||||
- 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
|
||||
- AppImage support ( see below )
|
||||
@@ -34,7 +77,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
|
||||
|
||||
@@ -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 *
|
||||
|
||||
2
aur/bauh-tray
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
/usr/bin/bauh --tray=1
|
||||
9
aur/bauh.desktop
Normal file
@@ -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
|
||||
11
aur/bauh_tray.desktop
Normal file
@@ -0,0 +1,11 @@
|
||||
[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=/usr/bin/bauh --tray=1
|
||||
Icon=bauh
|
||||
14
aur/copy_icons.py
Normal file
@@ -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('.')[0]
|
||||
dest_dir = output_base + '/' + res + '/apps'
|
||||
|
||||
Path(dest_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.copy(f, dest_dir + '/bauh.png')
|
||||
BIN
aur/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
aur/icons/12x12.png
Normal file
|
After Width: | Height: | Size: 889 B |
BIN
aur/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
aur/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
aur/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
aur/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
aur/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
aur/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
aur/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
aur/icons/8x8.png
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
aur/icons/96x96.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
@@ -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))
|
||||
@@ -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)
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '0.7.0'
|
||||
__version__ = '0.7.1'
|
||||
__app_name__ = 'bauh'
|
||||
|
||||
import os
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
108
bauh/commons/category.py
Normal file
@@ -0,0 +1,108 @@
|
||||
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(len(categories), self.manager.__class__.__name__)))
|
||||
self.manager.categories = categories
|
||||
|
||||
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'))
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
@@ -388,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]:
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
@@ -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.
|
||||
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.
|
||||
@@ -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.
|
||||
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.
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
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'
|
||||
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'
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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, CategoriesDownloader
|
||||
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'
|
||||
@@ -33,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):
|
||||
|
||||
@@ -51,11 +59,12 @@ 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_map = {}
|
||||
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):
|
||||
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'])
|
||||
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories)
|
||||
app.downgrade_enabled = downgrade_enabled
|
||||
|
||||
if app.installed:
|
||||
@@ -66,7 +75,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,10 +84,12 @@ 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)
|
||||
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()
|
||||
@@ -117,9 +128,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)
|
||||
pkg.downgrade_enabled = downgrade_enabled
|
||||
pkg.categories = self.categories_map.get(pkg.name)
|
||||
|
||||
if disk_loader:
|
||||
disk_loader.fill(pkg)
|
||||
@@ -137,7 +147,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:
|
||||
@@ -161,6 +171,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)
|
||||
|
||||
@@ -490,6 +501,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:
|
||||
@@ -559,7 +576,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)
|
||||
|
||||
@@ -678,7 +695,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'
|
||||
@@ -687,16 +704,13 @@ 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
|
||||
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:
|
||||
@@ -725,9 +739,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), suggestions.ALL.get(pkg['Name'])))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -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,8 +138,10 @@ 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)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,9 +7,9 @@ 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+|[\.\-_@#]+')
|
||||
|
||||
BAUH_PACKAGES = {'bauh', 'bauh-staging'}
|
||||
RE_SFX = ('r', 're', 'release')
|
||||
GA_SFX = ('ga', 'ge')
|
||||
RC_SFX = ('rc',)
|
||||
@@ -49,46 +49,47 @@ 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))
|
||||
|
||||
latest_split = RE_VERSION_DELS.split(latest_version)
|
||||
version_split = RE_VERSION_DELS.split(version)
|
||||
return nlatest > nversion
|
||||
|
||||
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 +97,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):
|
||||
@@ -104,14 +113,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
|
||||
|
||||
@@ -2,12 +2,10 @@ 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, ARCH_CACHE_PATH
|
||||
|
||||
from bauh.gems.arch import ROOT_DIR
|
||||
|
||||
CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry'}
|
||||
CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry', 'categories'}
|
||||
|
||||
|
||||
class ArchPackage(SoftwarePackage):
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -13,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 = {}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -124,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:
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
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 prevent damage to your system.
|
||||
@@ -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 {}
|
||||
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.
|
||||
@@ -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 {}
|
||||
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.
|
||||
@@ -2,18 +2,15 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from math import ceil
|
||||
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 bauh.api.http import HttpClient
|
||||
from bauh.gems.arch import pacman, disk
|
||||
|
||||
URL_INDEX = 'https://aur.archlinux.org/packages.gz'
|
||||
@@ -47,7 +44,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
|
||||
@@ -137,34 +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 CategoriesDownloader:
|
||||
|
||||
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt'
|
||||
|
||||
def __init__(self, http_client: HttpClient, logger: logging.Logger):
|
||||
self.http_client = http_client
|
||||
self.logger = logger
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
return {}
|
||||
else:
|
||||
self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
@@ -24,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
|
||||
@@ -45,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)
|
||||
@@ -247,7 +250,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']]
|
||||
@@ -279,11 +282,20 @@ 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 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import traceback
|
||||
from io import StringIO
|
||||
from threading import Thread
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
@@ -12,7 +13,7 @@ from bauh.gems.flatpak.model import FlatpakApplication
|
||||
|
||||
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 +21,21 @@ class FlatpakAsyncDataLoader(Thread):
|
||||
self.api_cache = api_cache
|
||||
self.persist = False
|
||||
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:
|
||||
@@ -51,7 +67,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 = self.format_category(c['name'])
|
||||
self.category_cache.add_non_existing(c['name'], cached)
|
||||
|
||||
cats.append(cached)
|
||||
|
||||
self.app.categories = cats
|
||||
|
||||
loaded_data = self.app.get_data_to_cache()
|
||||
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
import os
|
||||
|
||||
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'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
@@ -8,12 +9,16 @@ 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.category import CategoriesDownloader
|
||||
from bauh.commons.html import bold
|
||||
from bauh.commons.system import SystemProcess, ProcessHandler
|
||||
from bauh.gems.snap import snap, suggestions
|
||||
from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess
|
||||
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.+)'))
|
||||
|
||||
|
||||
class SnapManager(SoftwareManager):
|
||||
@@ -28,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.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'),
|
||||
@@ -41,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()
|
||||
|
||||
@@ -88,6 +108,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:
|
||||
@@ -125,9 +146,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
|
||||
@@ -148,20 +191,24 @@ 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
|
||||
|
||||
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']))]
|
||||
|
||||
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'))]
|
||||
|
||||
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))
|
||||
|
||||
@@ -182,6 +229,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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.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 below ( select one if you want to continue )
|
||||
@@ -12,5 +12,8 @@ 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.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 abajo ( seleccione uno si desea continuar )
|
||||
@@ -12,5 +12,8 @@ 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.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 abaixo ( selecione algum se quiser continuar )
|
||||
@@ -1,9 +1,10 @@
|
||||
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
|
||||
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 +177,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:
|
||||
@@ -226,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
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from threading import Thread
|
||||
from typing import Dict, List
|
||||
|
||||
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
|
||||
from bauh.gems.snap.constants import SNAP_API_URL
|
||||
from bauh.gems.snap.model import SnapApplication
|
||||
@@ -72,34 +68,3 @@ class SnapAsyncDataLoader(Thread):
|
||||
|
||||
if self.persist:
|
||||
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
|
||||
|
||||
|
||||
class CategoriesDownloader:
|
||||
|
||||
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt'
|
||||
|
||||
def __init__(self, http_client: HttpClient, logger: logging.Logger):
|
||||
self.http_client = http_client
|
||||
self.logger = logger
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -55,8 +55,8 @@ class AboutDialog(QDialog):
|
||||
layout.addWidget(gems_widget)
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_version = QLabel(locale_keys['version'].lower() + ': ' + __version__)
|
||||
label_version.setStyleSheet('QLabel { font-size: 10px; font-weight: bold }')
|
||||
label_version = QLabel(locale_keys['version'].lower() + ' ' + __version__)
|
||||
label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }')
|
||||
label_version.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_version)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -64,11 +73,14 @@ 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()
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -314,6 +315,13 @@ class ManageWindow(QWidget):
|
||||
self._update_table(pkgs_info=pkgs_info, signal=True)
|
||||
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
|
||||
self.thread_verify_models.start()
|
||||
|
||||
def _finish_apply_filters_async(self, success: bool):
|
||||
self.label_status.setText('')
|
||||
self.ref_toolbar_search.setVisible(True)
|
||||
@@ -400,6 +408,21 @@ 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:
|
||||
for c in p.model.categories:
|
||||
categories.add(c.lower())
|
||||
|
||||
if categories:
|
||||
self._update_categories(categories, keep_selected=True)
|
||||
|
||||
def changeEvent(self, e: QEvent):
|
||||
if isinstance(e, QWindowStateChangeEvent):
|
||||
self._maximized = self.isMaximized()
|
||||
@@ -431,7 +454,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:
|
||||
@@ -439,7 +461,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
|
||||
@@ -449,8 +471,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 and res['types'])
|
||||
self.first_refresh = False
|
||||
self.recent_uninstall = False
|
||||
self._hide_fields_after_recent_installation()
|
||||
|
||||
def uninstall_app(self, app: PackageView):
|
||||
@@ -464,7 +487,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
|
||||
@@ -483,6 +506,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():
|
||||
@@ -547,7 +571,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:
|
||||
@@ -556,13 +580,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)
|
||||
|
||||
@@ -586,7 +611,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,15 +645,19 @@ class ManageWindow(QWidget):
|
||||
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)
|
||||
if not keep_filters:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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']
|
||||
|
||||
@@ -640,9 +669,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:
|
||||
@@ -660,19 +686,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:
|
||||
@@ -681,28 +713,48 @@ 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
|
||||
cat_list = list(categories)
|
||||
cat_list.sort()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -809,7 +861,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)
|
||||
@@ -835,8 +887,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)
|
||||
|
||||
@@ -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
|
||||
@@ -140,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
|
||||
screenshots.download.no_response=Image not found
|
||||
continue=continue
|
||||
stable=stable
|
||||
@@ -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í
|
||||
@@ -181,8 +182,12 @@ filetransfer=transferencia de archivos
|
||||
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
|
||||
screenshots.download.no_content=No hay contenido para mostrar
|
||||
screenshots.download.no_response=Imagen no encontrada
|
||||
screenshots.download.no_response=Imagen no encontrada
|
||||
continue=continuar
|
||||
stable=estable
|
||||
@@ -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
|
||||
@@ -184,8 +185,12 @@ viewer=visualizador
|
||||
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
|
||||
screenshots.download.no_content=Sem conteúdo para exibir
|
||||
screenshots.download.no_response=Imagem não encontrada
|
||||
screenshots.download.no_response=Imagem não encontrada
|
||||
continue=continuar
|
||||
stable=estável
|
||||
2
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"
|
||||
|
||||
@@ -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'))
|
||||
@@ -35,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
|
||||
@@ -148,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'))
|
||||
|
||||
23
tests/gems/arch/pacman_test.py
Normal file
@@ -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))
|
||||
98
tests/gems/arch/resources/pacman.conf
Normal file
@@ -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
|
||||
101
tests/gems/arch/resources/pacman_ign_pkgs.conf
Normal file
@@ -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
|
||||
0
tests/gems/flatpak/__init__.py
Normal file
20
tests/gems/flatpak/worker_test.py
Normal file
@@ -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'))
|
||||