add gpg public_key ipmort
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -21,6 +21,8 @@ test_extract.py
|
||||
test_paths.py
|
||||
test_resources.py
|
||||
manager_fixed.py
|
||||
test_simple.sh
|
||||
test_container.sh
|
||||
|
||||
# Docker-Build
|
||||
docker_build/
|
||||
@ -28,3 +30,5 @@ debug_docker.sh
|
||||
Dockerfile.nuitka
|
||||
DOCKER_BUILD_ANLEITUNG.md
|
||||
nuitka_builder.py
|
||||
Dockerfile.test
|
||||
Dockerfile.simple
|
||||
|
40
Changelog
40
Changelog
@ -2,7 +2,41 @@ Changelog for LXTools installer
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
-
|
||||
- In the future, lxtools_installer will use the /tmp working
|
||||
directory for extracting necessary files from the AppImage. Currently,
|
||||
required files are extracted into separate folders using additional
|
||||
methods, which would then be removed. Depending on how the installer
|
||||
is called, examples include being invoked in the home directory where
|
||||
needed folders and files are unpacked there, and upon closing the app,
|
||||
these unpacked files will be automatically deleted. Additionally,
|
||||
a folder is created in the /tmp directory. This folder is used
|
||||
when the installer is called from an installed program to avoid
|
||||
permission issues and to prevent extracting into a bin folder.
|
||||
|
||||
### Added
|
||||
09.07.2025
|
||||
|
||||
- gpg check, download and import public_key.asc from two sources
|
||||
automatically and check if the signature is valid
|
||||
|
||||
- Check checksumm and signature of the AppImage-File
|
||||
|
||||
- Methods for checking pkexec and NetworkManager extended for Fedora
|
||||
and Open Suse if it is displayed incorrectly, the appimage can be started
|
||||
in terminal to see where the problem is
|
||||
|
||||
### Added
|
||||
02.07.2025
|
||||
|
||||
- build dockercontainer (ubuntu 22.04) for build appimage
|
||||
the app installer is now running on Debian 12
|
||||
|
||||
- first complete test runs on Debian12, Linux Mint 22.1,
|
||||
Open Suse (Leap and Thumbleweed) and Fedora,
|
||||
at Arch linux the installer starts only if xorg-xrandr is missing
|
||||
|
||||
- the installable programs also run on all systems mentioned
|
||||
and should also be running on other derivatives.
|
||||
|
||||
|
||||
### Added
|
||||
@ -16,6 +50,7 @@ Changelog for LXTools installer
|
||||
- add methods check polkit and check Networkmanager is installed
|
||||
and view in header is result false
|
||||
|
||||
|
||||
### Added
|
||||
23-06-2025
|
||||
|
||||
@ -37,6 +72,7 @@ Changelog for LXTools installer
|
||||
version ud the respective recognized system to run with
|
||||
it on all supported systems.
|
||||
|
||||
|
||||
### Added
|
||||
21-06-2025
|
||||
|
||||
@ -57,6 +93,7 @@ Changelog for LXTools installer
|
||||
|
||||
- Installer divided into several modules and added new MessageDialog module
|
||||
|
||||
|
||||
### Added
|
||||
4-06-2025
|
||||
|
||||
@ -65,6 +102,7 @@ Changelog for LXTools installer
|
||||
|
||||
- add ensure_shared_libs_pth_exists Script to install
|
||||
|
||||
|
||||
### Added
|
||||
4-06-2025
|
||||
|
||||
|
30
README.md
30
README.md
@ -2,6 +2,36 @@
|
||||
|
||||
LX Tools Installer is a GUI for simple install, update, and remove Apps from ilunix.de
|
||||
|
||||
## Build
|
||||
|
||||
Create a compatible AppImage:
|
||||
```bash
|
||||
./build_compatible.sh
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `lxtools_installer{VERSION}-x86_64.AppImage` - The executable
|
||||
- `lxtools_installer{VERSION}-x86_64.AppImage.sha256` - SHA256 checksum
|
||||
- `lxtools_installer{VERSION}-x86_64.AppImage.asc` - GPG signature (if configured)
|
||||
|
||||
## GPG Setup
|
||||
|
||||
To enable GPG signing:
|
||||
```bash
|
||||
./gpg_setup.sh
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Verify the downloaded AppImage:
|
||||
```bash
|
||||
# Check SHA256
|
||||
sha256sum -c lxtools_installer{VERSION}-x86_64.AppImage.sha256
|
||||
|
||||
# Verify GPG signature (requires public key)
|
||||
gpg --import public_key.asc
|
||||
gpg --verify lxtools_installer{VERSION}-x86_64.AppImage.asc lxtools_installer{VERSION}-x86_64.AppImage
|
||||
```
|
||||
|
||||
# Screenshots
|
||||
[](https://fb.ilunix.de/share/ZnfG9gxv)
|
51
gpg_setup.sh
Executable file
51
gpg_setup.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔐 GPG Setup für lxtools_installer"
|
||||
echo "=================================="
|
||||
|
||||
# Check if GPG is installed
|
||||
if ! command -v gpg &> /dev/null; then
|
||||
echo "❌ GPG ist nicht installiert"
|
||||
echo "Installation: sudo apt install gnupg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if GPG key exists
|
||||
if ! gpg --list-secret-keys | grep -q "sec"; then
|
||||
echo "📋 Kein GPG-Schlüssel gefunden. Erstelle einen neuen..."
|
||||
echo "Verwende diese Einstellungen:"
|
||||
echo "- Typ: RSA"
|
||||
echo "- Größe: 4096"
|
||||
echo "- Gültigkeitsdauer: 2y (2 Jahre)"
|
||||
echo "- Email: deine@email.com"
|
||||
echo ""
|
||||
gpg --full-generate-key
|
||||
else
|
||||
echo "✅ GPG-Schlüssel bereits vorhanden"
|
||||
fi
|
||||
|
||||
# List available keys
|
||||
echo ""
|
||||
echo "📋 Verfügbare Schlüssel:"
|
||||
gpg --list-secret-keys --keyid-format SHORT
|
||||
|
||||
# Ask for key selection
|
||||
echo ""
|
||||
read -p "Welchen Key-ID möchtest du verwenden? (oder Enter für Standard): " KEY_ID
|
||||
|
||||
if [ -z "$KEY_ID" ]; then
|
||||
# Use first available key
|
||||
KEY_ID=$(gpg --list-secret-keys --keyid-format SHORT | grep "sec" | head -1 | sed 's/.*\///' | cut -d' ' -f1)
|
||||
fi
|
||||
|
||||
echo "🔑 Verwende Key-ID: $KEY_ID"
|
||||
|
||||
# Export public key
|
||||
gpg --export --armor "$KEY_ID" > public_key.asc
|
||||
|
||||
echo "✅ Öffentlicher Schlüssel exportiert: public_key.asc"
|
||||
echo ""
|
||||
echo "📋 Nächste Schritte:"
|
||||
echo "1. Committen Sie public_key.asc ins Repository"
|
||||
echo "2. Testen Sie den Build mit: ./build_compatible.sh"
|
||||
echo "3. Verifizieren Sie die Signatur mit: gpg --verify file.asc file"
|
70
gpg_simple_setup.sh
Executable file
70
gpg_simple_setup.sh
Executable file
@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔐 Einfaches GPG Setup für lxtools_installer"
|
||||
echo "==========================================="
|
||||
|
||||
# Check if GPG is installed
|
||||
if ! command -v gpg &> /dev/null; then
|
||||
echo "❌ GPG ist nicht installiert"
|
||||
echo "Installation: sudo apt install gnupg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if GPG key exists
|
||||
if ! gpg --list-secret-keys | grep -q "sec"; then
|
||||
echo "📋 Erstelle GPG-Schlüssel automatisch..."
|
||||
echo ""
|
||||
echo "Verwende diese Einstellungen:"
|
||||
echo "- Name: Désiré Werner Menrath"
|
||||
echo "- Email: polunga40@unity-mail.de"
|
||||
echo "- Typ: RSA 4096"
|
||||
echo "- Gültigkeitsdauer: 2 Jahre"
|
||||
echo ""
|
||||
|
||||
# Create GPG key non-interactively
|
||||
cat > /tmp/gpg_batch <<EOF
|
||||
Key-Type: RSA
|
||||
Key-Length: 4096
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 4096
|
||||
Name-Real: Désiré Werner Menrath
|
||||
Name-Email: polunga40@unity-mail.de
|
||||
Expire-Date: 2y
|
||||
%no-protection
|
||||
%commit
|
||||
EOF
|
||||
|
||||
echo "🔑 Erstelle Schlüssel..."
|
||||
gpg --batch --generate-key /tmp/gpg_batch
|
||||
rm /tmp/gpg_batch
|
||||
|
||||
echo "✅ GPG-Schlüssel erstellt!"
|
||||
else
|
||||
echo "✅ GPG-Schlüssel bereits vorhanden"
|
||||
fi
|
||||
|
||||
# Get the key ID
|
||||
KEY_ID=$(gpg --list-secret-keys --keyid-format SHORT | grep "sec" | head -1 | sed 's/.*\///' | cut -d' ' -f1)
|
||||
|
||||
echo ""
|
||||
echo "🔑 Verwende Key-ID: $KEY_ID"
|
||||
|
||||
# Export public key
|
||||
gpg --export --armor "$KEY_ID" > public_key.asc
|
||||
|
||||
echo "✅ Öffentlicher Schlüssel exportiert: public_key.asc"
|
||||
|
||||
# Test the signing
|
||||
echo ""
|
||||
echo "🧪 Teste Signierung..."
|
||||
echo "test" > /tmp/test.txt
|
||||
if gpg --armor --detach-sign --yes /tmp/test.txt 2>/dev/null; then
|
||||
echo "✅ Signierung funktioniert!"
|
||||
rm /tmp/test.txt /tmp/test.txt.asc
|
||||
else
|
||||
echo "❌ Signierung fehlgeschlagen"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📋 Setup abgeschlossen!"
|
||||
echo "Jetzt kannst du ./build_compatible.sh ausführen"
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 757 B |
BIN
lx-icons/32/lxtools_key.png
Normal file
BIN
lx-icons/32/lxtools_key.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -4,6 +4,8 @@ from tkinter import ttk
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import Optional, List
|
||||
import hashlib
|
||||
import getpass
|
||||
from datetime import datetime
|
||||
import tempfile
|
||||
@ -18,8 +20,9 @@ from manager import (
|
||||
Image,
|
||||
AppManager,
|
||||
LxTools,
|
||||
Locale,
|
||||
)
|
||||
from network import NetworkChecker
|
||||
from network import NetworkChecker, GiteaUpdater, GPGManager
|
||||
from message import MessageDialog
|
||||
|
||||
|
||||
@ -34,6 +37,173 @@ class InstallationManager:
|
||||
self.system_manager = System()
|
||||
self.download_manager = DownloadManager()
|
||||
|
||||
def check_gpg(self) -> bool:
|
||||
if GPGManager.is_key_already_imported(
|
||||
LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:]
|
||||
):
|
||||
return True
|
||||
|
||||
all_keys_valid: bool = True
|
||||
|
||||
# 1. Check reachability of OpenPGP keyserver and process key if reachable
|
||||
openpgp_reachable: bool = GPGManager.is_url_reachable(
|
||||
LXToolsAppConfig.KEY_URL_OPENPGP
|
||||
)
|
||||
if openpgp_reachable:
|
||||
self.log(LocaleStrings.MSGGPG["keyserver_reachable"])
|
||||
self.update_progress(LocaleStrings.MSGGPG["keyserver_reachable"])
|
||||
if not GPGManager.import_key_from_url(
|
||||
key_url=LXToolsAppConfig.KEY_URL_OPENPGP,
|
||||
expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT,
|
||||
filename="public_key1.asc",
|
||||
):
|
||||
all_keys_valid = False
|
||||
else:
|
||||
self.log(LocaleStrings.MSGGPG["keyserver_unreachable"])
|
||||
self.update_progress(LocaleStrings.MSGGPG["keyserver_unreachable"])
|
||||
|
||||
# 2. Always download and verify Git.ilunix public key
|
||||
if not GPGManager.import_key_from_url(
|
||||
key_url=LXToolsAppConfig.KEY_URL_GITILUNIX,
|
||||
expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT,
|
||||
filename="public_key2.asc",
|
||||
):
|
||||
all_keys_valid = False
|
||||
|
||||
# Final step: Proceed only if all fingerprints match or OpenPGP was unreachable
|
||||
if all_keys_valid:
|
||||
self.log(LocaleStrings.MSGGPG["all_keys_valid"])
|
||||
self.update_progress(LocaleStrings.MSGGPG["all_keys_valid"])
|
||||
key_id: str = LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:]
|
||||
|
||||
if not GPGManager.update_gpg_trust_level(key_id, trust_level=5):
|
||||
self.log(LocaleStrings.MSGGPG["error_updating_trust_level"], key_id)
|
||||
self.update_progress(
|
||||
LocaleStrings.MSGGPG["error_updating_trust_level"], key_id
|
||||
)
|
||||
return False
|
||||
else:
|
||||
self.log(f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}.")
|
||||
self.update_progress(
|
||||
f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}."
|
||||
)
|
||||
return True
|
||||
|
||||
else:
|
||||
self.log(LocaleStrings.MSGGPG["not_all_keys_are_valid"])
|
||||
self.update_progress(LocaleStrings.MSGGPG["not_all_keys_are_valid"])
|
||||
|
||||
def check_appimage(self) -> bool | None | str:
|
||||
"""
|
||||
Checks the current version of the lxtools_installer AppImage by:
|
||||
|
||||
1. Downloading it from Gitea (with checksum and signature verification).
|
||||
2. Renaming and making it executable.
|
||||
3. Comparing its SHA-256 hash with an existing reference file at `/usr/local/bin/lxtools_installer`.
|
||||
4. Returns `True` if the hashes match, otherwise `False`.
|
||||
|
||||
Returns:
|
||||
bool: True if AppImages are identical; False if they differ or an error occurred.
|
||||
"""
|
||||
appimage_path: Optional[str] = GiteaUpdater.download_appimage(
|
||||
verify_checksum=True, verify_signature=True
|
||||
)
|
||||
|
||||
tests: List[str] = [
|
||||
"Checksum file not found",
|
||||
"Signature file not found",
|
||||
"Checksum mismatch",
|
||||
"Signature verification failed",
|
||||
]
|
||||
|
||||
if appimage_path is None or appimage_path in tests:
|
||||
print(f"{LocaleStrings.MSGA['error']}{appimage_path}")
|
||||
return None
|
||||
|
||||
else:
|
||||
try:
|
||||
if os.path.isfile(appimage_path):
|
||||
|
||||
new_filename = "/tmp/portinstaller/lxtools_installer"
|
||||
|
||||
result = subprocess.run(
|
||||
["mv", appimage_path, new_filename],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.update_progress(LocaleStrings.MSGA["appimage_renamed"])
|
||||
self.log(LocaleStrings.MSGA["appimage_renamed"])
|
||||
else:
|
||||
self.update_progress(
|
||||
f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}"
|
||||
)
|
||||
self.log(
|
||||
f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}"
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["chmod", "+x", new_filename],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.update_progress(
|
||||
LocaleStrings.MSGA["appimage_executable_success"]
|
||||
)
|
||||
self.log(LocaleStrings.MSGA["appimage_executable_success"])
|
||||
else:
|
||||
self.update_progress(
|
||||
f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}"
|
||||
)
|
||||
self.log(
|
||||
f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}"
|
||||
)
|
||||
|
||||
# Define reference path
|
||||
reference_appimage_path = "/usr/local/bin/lxtools_installer"
|
||||
if os.path.isfile(reference_appimage_path):
|
||||
|
||||
def calculate_sha256(file_path: str) -> str:
|
||||
"""
|
||||
Calculates the SHA-256 hash of a file.
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file.
|
||||
|
||||
Returns:
|
||||
str: Hexdigest of the SHA-256 hash.
|
||||
"""
|
||||
hash_obj = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_obj.update(chunk)
|
||||
return hash_obj.hexdigest()
|
||||
|
||||
# Calculate hashes
|
||||
new_checksum = calculate_sha256(new_filename)
|
||||
reference_checksum = calculate_sha256(reference_appimage_path)
|
||||
|
||||
# Compare and decide action
|
||||
if new_checksum == reference_checksum:
|
||||
return "True" # String as a compatibility solution to network.py
|
||||
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
self.update_progress(LocaleStrings.MSGA["appimage_not_exist"])
|
||||
self.log(LocaleStrings.MSGA["appimage_not_exist"])
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.update_progress(f"Exception occurred: {str(e)}")
|
||||
self.log(f"Exception occurred: {str(e)}")
|
||||
return None
|
||||
|
||||
def install_project(self, project_key):
|
||||
"""Install or update project"""
|
||||
project_info = self.app_manager.get_project_info(project_key)
|
||||
@ -77,6 +247,13 @@ class InstallationManager:
|
||||
raise Exception(f"{LocaleStrings.MSGI['unknow_project']}{project_key}")
|
||||
|
||||
def _create_wirepy_install_script(self):
|
||||
gpg_checker = self.check_gpg()
|
||||
result_appimage = self.check_appimage()
|
||||
if not gpg_checker:
|
||||
print(gpg_checker)
|
||||
return
|
||||
if result_appimage is True or result_appimage is None:
|
||||
return
|
||||
detected_os = Detector.get_os()
|
||||
if detected_os == "Arch Linux":
|
||||
result_unzip = Detector.get_unzip()
|
||||
@ -90,8 +267,9 @@ class InstallationManager:
|
||||
"""Create Wire-Py installation script"""
|
||||
script = f"""#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Wire-Py Installation ==="
|
||||
if [ "{result_appimage}" = "False" ]; then
|
||||
cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer
|
||||
fi
|
||||
if [ "{detected_os}" = "Arch Linux" ]; then
|
||||
if [ "{result_unzip}" = "False" ]; then
|
||||
pacman -S --noconfirm unzip
|
||||
@ -215,13 +393,21 @@ fi
|
||||
|
||||
# Cleanup
|
||||
cd /tmp
|
||||
rm -rf wirepy_install
|
||||
rm -rf wirepy_install portinstaller
|
||||
|
||||
echo "Wire-Py installation completed!"
|
||||
"""
|
||||
return script
|
||||
|
||||
def _create_logviewer_install_script(self):
|
||||
gpg_checker = self.check_gpg()
|
||||
result_appimage = self.check_appimage()
|
||||
if not gpg_checker:
|
||||
print(gpg_checker)
|
||||
return
|
||||
|
||||
if result_appimage is True or result_appimage is None:
|
||||
return
|
||||
detected_os = Detector.get_os()
|
||||
if detected_os == "Arch Linux":
|
||||
result_unzip = Detector.get_unzip()
|
||||
@ -235,7 +421,10 @@ echo "Wire-Py installation completed!"
|
||||
script = f"""#!/bin/bash
|
||||
set -e
|
||||
{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface"
|
||||
echo "=== LogViewer Installation ==="
|
||||
|
||||
if [ "{result_appimage}" = "False" ]; then
|
||||
cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer
|
||||
fi
|
||||
if [ "{detected_os}" = "Arch Linux" ]; then
|
||||
if [ "{result_unzip}" = "False" ]; then
|
||||
pacman -S --noconfirm unzip
|
||||
@ -315,7 +504,7 @@ fi
|
||||
|
||||
# Cleanup
|
||||
cd /tmp
|
||||
rm -rf logviewer_install
|
||||
rm -rf logviewer_install portinstaller
|
||||
|
||||
echo "LogViewer installation completed!"
|
||||
"""
|
||||
@ -344,7 +533,7 @@ echo "LogViewer installation completed!"
|
||||
|
||||
# Log output
|
||||
if result.stdout:
|
||||
self.log(f"STDOUT: {result.stdout}")
|
||||
self.log((result.stdout).strip("Log\n"))
|
||||
if result.stderr:
|
||||
self.log(f"STDERR: {result.stderr}")
|
||||
|
||||
@ -358,6 +547,7 @@ echo "LogViewer installation completed!"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception(LocaleStrings.MSGI["install_timeout"])
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception(f"{LocaleStrings.MSGI['install_script_failed']}{e}")
|
||||
|
||||
@ -476,6 +666,7 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then
|
||||
echo "No other LX apps found, removing shared resources..."
|
||||
rm -rf /usr/share/icons/lx-icons
|
||||
rm -rf /usr/share/TK-Themes
|
||||
rm -rf /usr/local/bin/lxtools_installer
|
||||
|
||||
# Remove shared libs
|
||||
for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do
|
||||
@ -523,6 +714,7 @@ if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then
|
||||
echo "No other LX apps found, removing shared resources..."
|
||||
rm -rf /usr/share/icons/lx-icons
|
||||
rm -rf /usr/share/TK-Themes
|
||||
rm -rf /usr/local/bin/lxtools_installer
|
||||
|
||||
# Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer)
|
||||
for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do
|
||||
@ -568,7 +760,7 @@ echo "LogViewer uninstallation completed!"
|
||||
|
||||
# Log output
|
||||
if result.stdout:
|
||||
self.log(f"STDOUT: {result.stdout}")
|
||||
self.log(result.stdout)
|
||||
if result.stderr:
|
||||
self.log(f"STDERR: {result.stderr}")
|
||||
|
||||
@ -628,6 +820,8 @@ class LXToolsGUI:
|
||||
self.download_icon_label = None
|
||||
self.log_text = None
|
||||
self.selected_project = None
|
||||
self.polkit_ok = Detector.get_polkit()
|
||||
self.networkmanager_ok = Detector.get_networkmanager()
|
||||
self.project_frames = {}
|
||||
self.status_labels = {}
|
||||
self.version_labels = {}
|
||||
@ -661,7 +855,7 @@ class LXToolsGUI:
|
||||
def create_gui(self):
|
||||
"""Create the main GUI"""
|
||||
self.root = tk.Tk()
|
||||
self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}")
|
||||
self.root.title(f"{Locale.APP_NAME} v{LXToolsAppConfig.VERSION}")
|
||||
self.root.geometry(
|
||||
f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100"
|
||||
)
|
||||
@ -677,6 +871,7 @@ class LXToolsGUI:
|
||||
self.root.iconphoto(False, icon)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT)
|
||||
Theme.apply_light_theme(self.root)
|
||||
# Create header
|
||||
@ -719,9 +914,24 @@ class LXToolsGUI:
|
||||
icon_text_frame = tk.Frame(left_side, bg="#2c3e50")
|
||||
icon_text_frame.pack(anchor="w")
|
||||
|
||||
# Tool-Icon
|
||||
# Tool-Icon (versuche echtes Icon, dann Fallback)
|
||||
try:
|
||||
icon = self.image_manager.load_image("header_image")
|
||||
if icon:
|
||||
# Resize icon für Header
|
||||
icon_label = tk.Label(icon_text_frame, image=icon, bg="#2c3e50")
|
||||
icon_label.image = icon # Referenz behalten
|
||||
icon_label.pack(side="left", padx=(0, 8))
|
||||
else:
|
||||
raise Exception("Kein Icon gefunden")
|
||||
except:
|
||||
# Fallback: Unicode-Symbol statt Emoji
|
||||
tk.Label(
|
||||
icon_text_frame, text="🔧", font=("Helvetica", 18), bg="#2c3e50", fg="white"
|
||||
icon_text_frame,
|
||||
text="⚙",
|
||||
font=("Helvetica", 18),
|
||||
bg="#2c3e50",
|
||||
fg="white",
|
||||
).pack(side="left", padx=(0, 8))
|
||||
|
||||
# App Name and Version
|
||||
@ -774,40 +984,51 @@ class LXToolsGUI:
|
||||
|
||||
def check_ready_status(self):
|
||||
"""Check if system is ready for installation"""
|
||||
# Checks:
|
||||
polkit_ok = Detector.get_polkit()
|
||||
networkmanager_ok = Detector.get_networkmanager()
|
||||
|
||||
# Führe alle Checks durch
|
||||
internet_ok = NetworkChecker.check_internet_connection()
|
||||
repo_ok = NetworkChecker.check_repository_access()
|
||||
result = Detector.get_host_python_version()
|
||||
python_result = Detector.get_host_python_version()
|
||||
|
||||
if not polkit_ok:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["polkit_check"], "#e74c3c"
|
||||
) # Red
|
||||
if not networkmanager_ok:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["networkmanager_check"], "#e74c3c"
|
||||
) # Red
|
||||
# Sammle alle Probleme
|
||||
issues = []
|
||||
if not self.polkit_ok:
|
||||
issues.append(("PolicyKit", "#e74c3c", LocaleStrings.MSGO["polkit_check"]))
|
||||
if not self.networkmanager_ok:
|
||||
issues.append(
|
||||
(
|
||||
"NetworkManager",
|
||||
"#e74c3c",
|
||||
LocaleStrings.MSGO["networkmanager_check"],
|
||||
)
|
||||
)
|
||||
if not internet_ok:
|
||||
issues.append(("Internet", "#e74c3c", LocaleStrings.MSGO["no_internet"]))
|
||||
if not repo_ok:
|
||||
issues.append(
|
||||
("Repository", "#f39c12", LocaleStrings.MSGO["repo_unavailable"])
|
||||
)
|
||||
if python_result is None:
|
||||
issues.append(("Python", "#e74c3c", LocaleStrings.MSGO["python_check"]))
|
||||
|
||||
if internet_ok and repo_ok and result is not None:
|
||||
self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green
|
||||
elif not internet_ok:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["no_internet"], "#e74c3c"
|
||||
) # Red
|
||||
elif not repo_ok:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["repo_unavailable"], "#f39c12"
|
||||
) # Orange
|
||||
elif result is None:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["python_check"], "#e74c3c"
|
||||
) # Red
|
||||
# Bestimme Hauptstatus basierend auf Priorität
|
||||
if issues:
|
||||
# Zeige das wichtigste Problem (Internet > Python > Repository > Services)
|
||||
for priority_check in [
|
||||
"Internet",
|
||||
"Python",
|
||||
"Repository",
|
||||
"PolicyKit",
|
||||
"NetworkManager",
|
||||
]:
|
||||
for issue_name, color, message in issues:
|
||||
if issue_name == priority_check:
|
||||
print(f"DEBUG: Zeige Problem: {issue_name} - {message}")
|
||||
self.update_header_status(message, color)
|
||||
return
|
||||
else:
|
||||
self.update_header_status(
|
||||
LocaleStrings.MSGO["system_check"], "#3498db"
|
||||
) # Blue
|
||||
|
||||
self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green
|
||||
|
||||
def _create_projects_tab(self):
|
||||
"""Create projects tab with project cards"""
|
||||
@ -1046,14 +1267,16 @@ class LXToolsGUI:
|
||||
clear_log_btn.pack(side="right", pady=(0, 10))
|
||||
|
||||
# Initial log message
|
||||
self.log_message(
|
||||
f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ==="
|
||||
)
|
||||
self.log_message(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===")
|
||||
self.log_message(f"{LocaleStrings.MSGL['work_dir']}{LXToolsAppConfig.WORK_DIR}")
|
||||
self.log_message(
|
||||
f"{LocaleStrings.MSGL['icons_dir']}{LXToolsAppConfig.ICONS_DIR}"
|
||||
)
|
||||
self.log_message(f"{LocaleStrings.MSGL['detected_os']}{self.detected_os}")
|
||||
self.log_message(f"{LocaleStrings.MSGO['polkit_check_log']}{self.polkit_ok}")
|
||||
self.log_message(
|
||||
f"{LocaleStrings.MSGO['networkmanager_check_log']}{self.networkmanager_ok}"
|
||||
)
|
||||
self.log_message(f"{LocaleStrings.MSGO['ready']}...")
|
||||
|
||||
def _create_progress_section(self):
|
||||
@ -1089,11 +1312,14 @@ class LXToolsGUI:
|
||||
button_frame = tk.Frame(self.root, bg=self.colors["bg"])
|
||||
button_frame.pack(fill="x", padx=15, pady=(5, 10))
|
||||
|
||||
# Button style configuration
|
||||
# Button style configuration (mit Error-Handling für Linux Mint)
|
||||
try:
|
||||
style = ttk.Style()
|
||||
|
||||
# Install button (green)
|
||||
style.configure("Install.TButton", foreground="#27ae60", font=("Helvetica", 11))
|
||||
style.configure(
|
||||
"Install.TButton", foreground="#27ae60", font=("Helvetica", 11)
|
||||
)
|
||||
style.map(
|
||||
"Install.TButton",
|
||||
foreground=[("active", "#14542f"), ("pressed", "#1e8449")],
|
||||
@ -1107,15 +1333,20 @@ class LXToolsGUI:
|
||||
"Uninstall.TButton",
|
||||
foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")],
|
||||
)
|
||||
|
||||
# Refresh button (blue)
|
||||
style.configure("Refresh.TButton", foreground="#3498db", font=("Helvetica", 11))
|
||||
style.configure(
|
||||
"Refresh.TButton", foreground="#3498db", font=("Helvetica", 11)
|
||||
)
|
||||
style.map(
|
||||
"Refresh.TButton",
|
||||
foreground=[("active", "#1e3747"), ("pressed", "#2980b9")],
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Style-Konfiguration fehlgeschlagen: {e}")
|
||||
# Fallback: verwende Standard-Styles
|
||||
|
||||
# Create buttons
|
||||
# Create buttons (mit Fallback für Style-Probleme)
|
||||
try:
|
||||
install_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["install"],
|
||||
@ -1123,8 +1354,15 @@ class LXToolsGUI:
|
||||
style="Install.TButton",
|
||||
padding=8,
|
||||
)
|
||||
except:
|
||||
install_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["install"],
|
||||
command=self.install_selected,
|
||||
)
|
||||
install_btn.pack(side="left", padx=(0, 10))
|
||||
|
||||
try:
|
||||
uninstall_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["uninstall"],
|
||||
@ -1132,15 +1370,29 @@ class LXToolsGUI:
|
||||
style="Uninstall.TButton",
|
||||
padding=8,
|
||||
)
|
||||
except:
|
||||
uninstall_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["uninstall"],
|
||||
command=self.uninstall_selected,
|
||||
)
|
||||
uninstall_btn.pack(side="left", padx=(0, 10))
|
||||
|
||||
try:
|
||||
refresh_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["refresh"],
|
||||
command=self.refresh_status,
|
||||
style="Refresh.TButton",
|
||||
width=30,
|
||||
padding=8,
|
||||
)
|
||||
except:
|
||||
refresh_btn = ttk.Button(
|
||||
button_frame,
|
||||
text=LocaleStrings.MSGB["refresh"],
|
||||
command=self.refresh_status,
|
||||
)
|
||||
refresh_btn.pack(side="right")
|
||||
|
||||
def update_download_icon(self, status):
|
||||
@ -1211,16 +1463,16 @@ class LXToolsGUI:
|
||||
if latest_version != "Unknown":
|
||||
if installed_version != f"v. {latest_version}":
|
||||
version_label.config(
|
||||
text=f"{LocaleStrings.MSGC['latest']}(v. {latest_version}) {LocaleStrings.MSGC['update_available']}",
|
||||
text=f"{LocaleStrings.MSGC['update_on']}(v. {latest_version}) {LocaleStrings.MSGC['available_lower']}",
|
||||
fg="orange",
|
||||
)
|
||||
self.log_message(
|
||||
f"{project_info['name']}: {LocaleStrings.MSGC['update_available']}(v. {latest_version})"
|
||||
f"{project_info['name']}: {LocaleStrings.MSGC['update_on']}(v. {latest_version} {LocaleStrings.MSGC['available_lower']})"
|
||||
)
|
||||
|
||||
else:
|
||||
version_label.config(
|
||||
text=f"{LocaleStrings.MSGC['latest']}: (v. {latest_version}) {LocaleStrings.MSGC['up_to_date']}",
|
||||
text=f"(v. {latest_version}) {LocaleStrings.MSGC['up_to_date']}",
|
||||
fg="green",
|
||||
)
|
||||
self.log_message(
|
||||
@ -1379,7 +1631,7 @@ class LXToolsGUI:
|
||||
|
||||
def main():
|
||||
"""Main function to start the application"""
|
||||
print(f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===")
|
||||
print(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===")
|
||||
print(f"{LocaleStrings.MSGL['working_dir']}{os.getcwd()}")
|
||||
|
||||
try:
|
||||
|
487
lxtoolsinstaller.pot
Normal file
487
lxtoolsinstaller.pot
Normal file
@ -0,0 +1,487 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-09 08:01+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: manager.py:735
|
||||
msgid "WireGuard VPN Manager with GUI"
|
||||
msgstr "WireGuard-VPN-Manager mit GUI"
|
||||
|
||||
#: manager.py:748
|
||||
msgid "System Log Viewer with GUI"
|
||||
msgstr "Systemprotokoll-Ansicht mit GUI"
|
||||
|
||||
#: manager.py:812
|
||||
msgid "Refreshing status and checking versions..."
|
||||
msgstr "Status aktualisieren und Versionen prüfen..."
|
||||
|
||||
#: manager.py:813
|
||||
msgid "Starting installation of "
|
||||
msgstr "Installation von "
|
||||
|
||||
#: manager.py:814
|
||||
msgid "Installing "
|
||||
msgstr "Installiere "
|
||||
|
||||
#: manager.py:815
|
||||
msgid " installation successfully!"
|
||||
msgstr " Installation erfolgreich!"
|
||||
|
||||
#: manager.py:816
|
||||
msgid "Installation failed: "
|
||||
msgstr "Installation fehlgeschlagen: "
|
||||
|
||||
#: manager.py:817
|
||||
msgid "Created install script: "
|
||||
msgstr "Installations-Skript erstellt: "
|
||||
|
||||
#: manager.py:818
|
||||
msgid "Installation script failed: "
|
||||
msgstr "Installations-Skript fehlgeschlagen: "
|
||||
|
||||
#: manager.py:819
|
||||
msgid "Installation timed out"
|
||||
msgstr "Installation abgelaufen"
|
||||
|
||||
#: manager.py:820
|
||||
msgid "Installed "
|
||||
msgstr "Installiert: "
|
||||
|
||||
#: manager.py:824
|
||||
msgid "Uninstalling "
|
||||
msgstr "Deinstalieren von "
|
||||
|
||||
#: manager.py:825
|
||||
msgid " uninstalled successfully!"
|
||||
msgstr " erfolgreich deinstalliert!"
|
||||
|
||||
#: manager.py:826
|
||||
msgid "Uninstallation failed: "
|
||||
msgstr "Deinstallation fehlgeschlagen: "
|
||||
|
||||
#: manager.py:827
|
||||
msgid "Created uninstall script: "
|
||||
msgstr "Deinstallations-Skript erstellt: "
|
||||
|
||||
#: manager.py:828
|
||||
msgid "Uninstallation script failed: "
|
||||
msgstr "Deinstallations-Skript fehlgeschlagen: "
|
||||
|
||||
#: manager.py:829
|
||||
msgid "Uninstallation timed out"
|
||||
msgstr "Deinstallation abgelaufen"
|
||||
|
||||
#: manager.py:833
|
||||
msgid "Unknown project: "
|
||||
msgstr "Unbekanntes Projekt: "
|
||||
|
||||
#: manager.py:834
|
||||
msgid " is not installed."
|
||||
msgstr " ist nicht installiert."
|
||||
|
||||
#: manager.py:835
|
||||
msgid "Downloading from "
|
||||
msgstr "Herunterladen von "
|
||||
|
||||
#: manager.py:836
|
||||
msgid "Extracting files..."
|
||||
msgstr "Dateien entpacken..."
|
||||
|
||||
#: manager.py:837
|
||||
msgid "Download failed: "
|
||||
msgstr "Herunterladen fehlgeschlagen: "
|
||||
|
||||
#: manager.py:838
|
||||
msgid "System: "
|
||||
msgstr "System: "
|
||||
|
||||
#: manager.py:839
|
||||
msgid "Linux App Installer"
|
||||
msgstr "Linux-App-Installer"
|
||||
|
||||
#: manager.py:840
|
||||
msgid "Ready for installation"
|
||||
msgstr "Bereit für Installation"
|
||||
|
||||
#: manager.py:841
|
||||
msgid "No internet connection"
|
||||
msgstr "Keine Internetverbindung"
|
||||
|
||||
#: manager.py:842
|
||||
msgid "Repository unavailable"
|
||||
msgstr "Repository nicht verfügbar"
|
||||
|
||||
#: manager.py:843
|
||||
msgid "System checking..."
|
||||
msgstr "Systemprüfung..."
|
||||
|
||||
#: manager.py:844
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
#: manager.py:845
|
||||
msgid "Progress"
|
||||
msgstr "Fortschritt"
|
||||
|
||||
#: manager.py:846
|
||||
msgid "Status refresh completed"
|
||||
msgstr "Statusaktualisierung abgeschlossen"
|
||||
|
||||
#: manager.py:847
|
||||
msgid "Python not installed"
|
||||
msgstr "Python nicht installiert"
|
||||
|
||||
#: manager.py:848
|
||||
msgid "Please install Polkit!"
|
||||
msgstr "Bitte installieren Sie Polkit!"
|
||||
|
||||
#: manager.py:849
|
||||
msgid "Please install Networkmanager!"
|
||||
msgstr "Bitte installieren Sie Networkmanager!"
|
||||
|
||||
#: manager.py:850
|
||||
msgid "Polkit check: "
|
||||
msgstr "Polkit-Prüfung: "
|
||||
|
||||
#: manager.py:851
|
||||
msgid "Networkmanager check: "
|
||||
msgstr "Networkmanager-Prüfung: "
|
||||
|
||||
#: manager.py:856
|
||||
msgid "Checking..."
|
||||
msgstr "Prüfen..."
|
||||
|
||||
#: manager.py:857
|
||||
msgid "Version: Checking..."
|
||||
msgstr "Version: Prüfen..."
|
||||
|
||||
#: manager.py:858
|
||||
msgid "Update on "
|
||||
msgstr "Aktualisierung am "
|
||||
|
||||
#: manager.py:859
|
||||
msgid "available"
|
||||
msgstr "verfügbar"
|
||||
|
||||
#: manager.py:860
|
||||
msgid "Up to date"
|
||||
msgstr "Auf dem neuesten Stand"
|
||||
|
||||
#: manager.py:861
|
||||
msgid "Latest unknown"
|
||||
msgstr "Neueste Version unbekannt"
|
||||
|
||||
#: manager.py:862
|
||||
msgid "Could not check latest version"
|
||||
msgstr "Kann aktuelle Version nicht prüfen"
|
||||
|
||||
#: manager.py:863
|
||||
msgid "Latest: Check failed"
|
||||
msgstr "Neueste: Prüfung fehlgeschlagen"
|
||||
|
||||
#: manager.py:864
|
||||
msgid "Version check failed"
|
||||
msgstr "Versionsprüfung fehlgeschlagen"
|
||||
|
||||
#: manager.py:865
|
||||
msgid "Not installed"
|
||||
msgstr "Nicht installiert"
|
||||
|
||||
#: manager.py:866
|
||||
msgid "Available "
|
||||
msgstr "Verfügbar "
|
||||
|
||||
#: manager.py:867
|
||||
msgid "Available unknown"
|
||||
msgstr "Verfügbarkeit unbekannt"
|
||||
|
||||
#: manager.py:868
|
||||
msgid "Available: Check failed"
|
||||
msgstr "Verfügbar: Prüfung fehlgeschlagen"
|
||||
|
||||
#: manager.py:873
|
||||
msgid "Selected project: "
|
||||
msgstr "Ausgewähltes Projekt: "
|
||||
|
||||
#: manager.py:874
|
||||
msgid "Installation Log"
|
||||
msgstr "Installationsprotokoll"
|
||||
|
||||
#: manager.py:875 manager.py:879
|
||||
msgid "Working directory: "
|
||||
msgstr "Arbeitsverzeichnis: "
|
||||
|
||||
#: manager.py:876
|
||||
msgid "Icons directory: "
|
||||
msgstr "Icons-Verzeichnis: "
|
||||
|
||||
#: manager.py:877
|
||||
msgid "Detected OS: "
|
||||
msgstr "Erkanntes Betriebssystem: "
|
||||
|
||||
#: manager.py:878
|
||||
msgid "Log cleared"
|
||||
msgstr "Protokoll geleert"
|
||||
|
||||
#: manager.py:880
|
||||
msgid ""
|
||||
"\n"
|
||||
"Application interrupted by user."
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Anwendung durch Benutzer unterbrochen."
|
||||
|
||||
#: manager.py:881
|
||||
msgid "Fatal error: "
|
||||
msgstr "Kritischer Fehler: "
|
||||
|
||||
#: manager.py:882
|
||||
msgid "Fatal Error Application failed to start: "
|
||||
msgstr "Kritischer Fehler: Anwendung konnte nicht gestartet werden: "
|
||||
|
||||
#: manager.py:887
|
||||
msgid "Clear Log"
|
||||
msgstr "Protokoll leeren"
|
||||
|
||||
#: manager.py:888
|
||||
msgid "Install/Update"
|
||||
msgstr "Installieren/Aktualisieren"
|
||||
|
||||
#: manager.py:889
|
||||
msgid "Uninstall"
|
||||
msgstr "Deinstallieren"
|
||||
|
||||
#: manager.py:890
|
||||
msgid "Refresh Status"
|
||||
msgstr "Status aktualisieren"
|
||||
|
||||
#: manager.py:895
|
||||
msgid "Please select a project to install."
|
||||
msgstr "Bitte ein Projekt zum Installieren auswählen."
|
||||
|
||||
#: manager.py:897
|
||||
msgid ""
|
||||
"No internet connection available.\n"
|
||||
"Please check your network connection."
|
||||
msgstr ""
|
||||
"Keine Internetverbindung vorhanden.\n"
|
||||
"Überprüfen Sie Ihre Netzwerkverbindung."
|
||||
|
||||
#: manager.py:900
|
||||
msgid ""
|
||||
"Cannot access repository.\n"
|
||||
"Please try again later."
|
||||
msgstr ""
|
||||
"Repository nicht erreichbar.\n"
|
||||
Bitte später erneut versuchen.
|
||||
|
||||
#: manager.py:902
|
||||
msgid "has been successfully installed/updated."
|
||||
msgstr "wurde erfolgreich installiert/aktualisiert."
|
||||
|
||||
#: manager.py:903
|
||||
msgid "Please select a project to uninstall."
|
||||
msgstr "Bitte ein Projekt zur Deinstallation auswählen."
|
||||
|
||||
#: manager.py:908
|
||||
msgid "Installing tkinter for "
|
||||
msgstr "Installation von tkinter für "
|
||||
|
||||
#: manager.py:909
|
||||
msgid "Command: "
|
||||
msgstr "Befehl: "
|
||||
|
||||
#: manager.py:910
|
||||
msgid "TKinter installation completed successfully!"
|
||||
msgstr "TKinter-Installation erfolgreich abgeschlossen!"
|
||||
|
||||
#: manager.py:911
|
||||
msgid "TKinter installation failed: "
|
||||
msgstr "TKinter-Installation fehlgeschlagen: "
|
||||
|
||||
#: manager.py:912
|
||||
msgid "TKinter installation timed out"
|
||||
msgstr "TKinter-Installation abgelaufen"
|
||||
|
||||
#: manager.py:913
|
||||
msgid "Error installing tkinter: "
|
||||
msgstr "Fehler bei der Installation von tkinter: "
|
||||
|
||||
#: manager.py:914
|
||||
msgid "No tkinter installation command defined for "
|
||||
msgstr "Kein Installationsbefehl für tkinter definiert für "
|
||||
|
||||
#: manager.py:915
|
||||
msgid "Failed to load image from "
|
||||
msgstr "Bild konnte nicht geladen werden aus "
|
||||
|
||||
#: manager.py:916
|
||||
msgid "LogViewer installation check:"
|
||||
msgstr "Prüfung der Installation von LogViewer:"
|
||||
|
||||
#: manager.py:917
|
||||
msgid " Symlink exists: "
|
||||
msgstr " Symbolischer Link existiert: "
|
||||
|
||||
#: manager.py:918
|
||||
msgid " Executable exists: "
|
||||
msgstr " Ausführbarer Datei existiert: "
|
||||
|
||||
#: manager.py:919
|
||||
msgid " Is executable: "
|
||||
msgstr " Ist ausführbar: "
|
||||
|
||||
#: manager.py:920
|
||||
msgid " Final result: "
|
||||
msgstr " Endergebnis: "
|
||||
|
||||
#: manager.py:921
|
||||
msgid "Error getting version for "
|
||||
msgstr "Fehler beim Abrufen der Version für "
|
||||
|
||||
#: manager.py:926
|
||||
msgid "Error verifying signature: "
|
||||
msgstr "Fehler bei der Prüfung der Signatur: "
|
||||
|
||||
#: manager.py:927
|
||||
msgid "Could not find GPG signature: "
|
||||
msgstr "GPG-Signatur nicht gefunden: "
|
||||
|
||||
#: manager.py:928
|
||||
msgid "SHA256-File not found: "
|
||||
msgstr "SHA256-Datei nicht gefunden: "
|
||||
|
||||
#: manager.py:929
|
||||
msgid "Would you like to continue?"
|
||||
msgstr "Möchten Sie fortfahren?"
|
||||
|
||||
#: manager.py:930
|
||||
msgid "SHA256 hash mismatch. File might be corrupted!"
|
||||
msgstr "SHA256-Hash passt nicht überein. Datei könnte beschädigt sein!"
|
||||
|
||||
#: manager.py:931
|
||||
msgid "Failed to retrieve version from Gitea API"
|
||||
msgstr "Konnte Version über die Gitea-API nicht abrufen"
|
||||
|
||||
#: manager.py:932
|
||||
msgid "Error retrieving latest version: "
|
||||
msgstr "Fehler beim Abrufen der neuesten Version: "
|
||||
|
||||
#: manager.py:933
|
||||
msgid "GPG verification successful. Signature is valid."
|
||||
msgstr "GPG-Prüfung erfolgreich. Die Signatur ist gültig."
|
||||
|
||||
#: manager.py:934
|
||||
msgid "Error GPG check: "
|
||||
msgstr "Fehler bei der GPG-Prüfung: "
|
||||
|
||||
#: manager.py:935
|
||||
msgid "Error downloading or verifying AppImage: "
|
||||
msgstr "Fehler beim Herunterladen oder Verifizieren der AppImage: "
|
||||
|
||||
#: manager.py:936
|
||||
msgid "Error accessing Gitea repository: "
|
||||
msgstr "Fehler beim Zugreifen auf das Gitea-Repository: "
|
||||
|
||||
#: manager.py:937
|
||||
msgid "Error: "
|
||||
msgstr "Fehler: "
|
||||
|
||||
#: manager.py:938
|
||||
msgid "The AppImage renamed..."
|
||||
msgstr "Die AppImage umbenannt..."
|
||||
|
||||
#: manager.py:939
|
||||
msgid "Error renaming the AppImage: "
|
||||
msgstr "Fehler beim Umbenennen der AppImage: "
|
||||
|
||||
#: manager.py:940
|
||||
msgid "Error making the AppImage executable: "
|
||||
msgstr "Fehler beim Markieren der AppImage als ausführbar: "
|
||||
|
||||
#: manager.py:941
|
||||
msgid "The AppImage has been made executable"
|
||||
msgstr "Die AppImage wurde als ausführbar markiert"
|
||||
|
||||
#: manager.py:942
|
||||
msgid "Error: The AppImage file does not exist."
|
||||
msgstr "Fehler: Die AppImage-Datei existiert nicht."
|
||||
|
||||
#: manager.py:947
|
||||
msgid ""
|
||||
"Warning: 'gpg' is not installed. Please install it to verify the AppImage "
|
||||
"signature."
|
||||
msgstr ""
|
||||
"Warnung: 'gpg' ist nicht installiert. Bitte installieren Sie es, um die "
|
||||
"AppImage-Signatur zu überprüfen."
|
||||
|
||||
#: manager.py:949
|
||||
msgid "URL not reachable: "
|
||||
msgstr "URL nicht erreichbar: "
|
||||
|
||||
#: manager.py:951
|
||||
msgid "No fingerprint found in the key. File might be corrupted or empty."
|
||||
msgstr "Kein Fingerabdruck in dem Schlüssel gefunden. Datei könnte beschädigt oder leer sein."
|
||||
|
||||
#: manager.py:953
|
||||
msgid "Fingerprint mismatch: Expected "
|
||||
msgstr "Fingerabdruck nicht übereinstimmend: Erwartet "
|
||||
|
||||
#: manager.py:954
|
||||
msgid "but got: "
|
||||
msgstr "aber erhalten: "
|
||||
|
||||
#: manager.py:955
|
||||
msgid "Failed to import public key: "
|
||||
msgstr "Fehler beim Importieren des öffentlichen Schlüssels: "
|
||||
|
||||
#: manager.py:956
|
||||
msgid "GPG fingerprint extraction failed: "
|
||||
msgstr "Extrahierung von GPG-Fingerabdruck fehlgeschlagen: "
|
||||
|
||||
#: manager.py:957
|
||||
msgid "Error importing GPG key from "
|
||||
msgstr "Fehler beim Importieren des GPG-Schlüssels aus "
|
||||
|
||||
#: manager.py:958
|
||||
msgid "Error updating trust level: "
|
||||
msgstr "Fehler beim Aktualisieren des Vertrauensniveaus: "
|
||||
|
||||
#: manager.py:959
|
||||
msgid "Error executing script to update trust level: "
|
||||
msgstr "Fehler beim Ausführen des Skripts zur Aktualisierung des Vertrauensniveaus: "
|
||||
|
||||
#: manager.py:961
|
||||
msgid "OpenPGP keyserver is reachable. Proceeding with download."
|
||||
msgstr "Der OpenPGP-Schlüsselserver ist erreichbar. Herunterladen wird fortgesetzt."
|
||||
|
||||
#: manager.py:964
|
||||
msgid "OpenPGP keyserver unreachable. Skipping this source."
|
||||
msgstr "Der OpenPGP-Schlüsselserver ist nicht erreichbar. Dieser Quelle wird übersprungen."
|
||||
|
||||
#: manager.py:967
|
||||
msgid "All keys have valid fingerprints matching the expected value."
|
||||
msgstr "Alle Schlüssel haben gültige Fingerabdrücke, die mit dem erwarteten Wert übereinstimmen."
|
||||
|
||||
#: manager.py:969
|
||||
msgid "Trust level 5 successfully applied to key "
|
||||
msgstr "Vertrauensniveau 5 erfolgreich auf Schlüssel angewendet "
|
||||
|
||||
#: manager.py:970
|
||||
msgid "Not all keys are valid."
|
||||
msgstr "Nicht alle Schlüssel sind gültig."
|
||||
|
157
manager.py
157
manager.py
@ -11,7 +11,28 @@ import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import stat
|
||||
from network import GiteaUpdate
|
||||
from network import GiteaUpdater
|
||||
|
||||
|
||||
class Locale:
|
||||
APP_NAME = "lxtoolsinstaller"
|
||||
# Locale settings
|
||||
LOCALE_DIR = "./locale/"
|
||||
|
||||
@staticmethod
|
||||
def setup_translations() -> gettext.gettext:
|
||||
"""Initialize translations and set the translation function"""
|
||||
try:
|
||||
locale.bindtextdomain(Locale.APP_NAME, Locale.LOCALE_DIR)
|
||||
gettext.bindtextdomain(Locale.APP_NAME, Locale.LOCALE_DIR)
|
||||
gettext.textdomain(Locale.APP_NAME)
|
||||
except:
|
||||
pass
|
||||
return gettext.gettext
|
||||
|
||||
|
||||
# Initialize translations
|
||||
_ = Locale.setup_translations()
|
||||
|
||||
|
||||
class Detector:
|
||||
@ -107,14 +128,16 @@ class Detector:
|
||||
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
|
||||
if os_system in deb:
|
||||
result = subprocess.run(
|
||||
["apt", "list", "--installed", "|", "grep", "polkit"],
|
||||
["apt list --installed | grep polkit"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system in arch:
|
||||
@ -127,30 +150,35 @@ class Detector:
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system == "Fedora":
|
||||
result = subprocess.run(
|
||||
["dnf", "list", "--installed", "|", "grep", "polkit"],
|
||||
["systemctl --type=service | grep polkit"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap":
|
||||
result = subprocess.run(
|
||||
["zypper", "search", "--installed-only", "|", "grep", "polkit"],
|
||||
["zypper search --installed-only | grep pkexec"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@ -161,14 +189,17 @@ class Detector:
|
||||
arch = ["Arch Linux", "Manjaro", "EndeavourOS", "ArcoLinux", "Garuda Linux"]
|
||||
if os_system in deb:
|
||||
result = subprocess.run(
|
||||
["apt", "list", "--installed", "|", "grep", "network-manager"],
|
||||
["apt list --installed | grep network-manager"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system in arch:
|
||||
@ -178,33 +209,40 @@ class Detector:
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system == "Fedora":
|
||||
|
||||
result = subprocess.run(
|
||||
["dnf", "list", "--installed", "|", "grep", "NetworkManager"],
|
||||
["which NetworkManager"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
check=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
elif os_system == "SUSE Tumbleweed" or os_system == "SUSE Leap":
|
||||
result = subprocess.run(
|
||||
["zypper", "search", "--installed-only", "|", "grep", "networkmanager"],
|
||||
["zypper search --installed-only | grep NetworkManager"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
check=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
@ -322,6 +360,10 @@ class Image:
|
||||
"./lx-icons/48/log.png",
|
||||
"/usr/share/icons/lx-icons/48/log.png",
|
||||
],
|
||||
"header_image": [
|
||||
"./lx-icons/32/lxtools_key.png",
|
||||
"/usr/share/icons/lx-icons/32/lxtools_key.png",
|
||||
],
|
||||
}
|
||||
|
||||
# Get paths to try
|
||||
@ -430,7 +472,7 @@ class AppManager:
|
||||
if not project_info:
|
||||
return "Unknown"
|
||||
|
||||
return GiteaUpdate.api_down(project_info["api_url"])
|
||||
return GiteaUpdater.get_latest_version_from_api(project_info["api_url"])
|
||||
|
||||
def check_other_apps_installed(self, exclude_key) -> bool:
|
||||
"""Check if other apps are still installed"""
|
||||
@ -658,24 +700,8 @@ class LXToolsAppConfig:
|
||||
os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def setup_translations() -> gettext.gettext:
|
||||
"""Initialize translations and set the translation function"""
|
||||
try:
|
||||
locale.bindtextdomain(
|
||||
LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR
|
||||
)
|
||||
gettext.bindtextdomain(
|
||||
LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR
|
||||
)
|
||||
gettext.textdomain(LXToolsAppConfig.APP_NAME)
|
||||
except:
|
||||
pass
|
||||
return gettext.gettext
|
||||
|
||||
VERSION = "1.1.7"
|
||||
APP_NAME = "lxtoolsinstaller"
|
||||
WINDOW_WIDTH = 450
|
||||
VERSION = "1.1.8"
|
||||
WINDOW_WIDTH = 460
|
||||
WINDOW_HEIGHT = 580
|
||||
# Working directory
|
||||
WORK_DIR = os.getcwd()
|
||||
@ -683,9 +709,6 @@ class LXToolsAppConfig:
|
||||
THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes")
|
||||
TEMP_DIR = "/tmp/lxtools"
|
||||
|
||||
# Locale settings
|
||||
LOCALE_DIR = "./locale/"
|
||||
|
||||
# Download URLs
|
||||
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
|
||||
SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip"
|
||||
@ -696,11 +719,20 @@ class LXToolsAppConfig:
|
||||
"https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
|
||||
)
|
||||
|
||||
# GPG required
|
||||
EXPECTED_FINGERPRINT = "743745087C6414E00F1EF84D4CCF06B6CE2A4C7F"
|
||||
KEY_URL_OPENPGP = (
|
||||
f"https://keys.openpgp.org/vks/v1/by-fingerprint/{EXPECTED_FINGERPRINT}"
|
||||
)
|
||||
KEY_URL_GITILUNIX = (
|
||||
"https://git.ilunix.de/punix/lxtools_installer/raw/branch/main/public_key.asc"
|
||||
)
|
||||
|
||||
# Project configurations
|
||||
PROJECTS = {
|
||||
"wirepy": {
|
||||
"name": "Wire-Py",
|
||||
"description": "WireGuard VPN Manager with GUI",
|
||||
"description": _("WireGuard VPN Manager with GUI"),
|
||||
"download_url": WIREPY_URL,
|
||||
"api_url": WIREPY_API_URL,
|
||||
"icon_key": "icon_vpn",
|
||||
@ -713,7 +745,7 @@ class LXToolsAppConfig:
|
||||
},
|
||||
"logviewer": {
|
||||
"name": "LogViewer",
|
||||
"description": "System Log Viewer with GUI",
|
||||
"description": _("System Log Viewer with GUI"),
|
||||
"download_url": SHARED_LIBS_URL,
|
||||
"api_url": SHARED_LIBS_API_URL,
|
||||
"icon_key": "icon_log",
|
||||
@ -772,8 +804,6 @@ class LXToolsAppConfig:
|
||||
|
||||
|
||||
LXToolsAppConfig.extract_data_files()
|
||||
# Initialize translations
|
||||
_ = LXToolsAppConfig.setup_translations()
|
||||
|
||||
|
||||
class LocaleStrings:
|
||||
@ -817,14 +847,16 @@ class LocaleStrings:
|
||||
"python_check": _("Python not installed"),
|
||||
"polkit_check": _("Please install Polkit!"),
|
||||
"networkmanager_check": _("Please install Networkmanager!"),
|
||||
"polkit_check_log": _("Polkit check: "),
|
||||
"networkmanager_check_log": _("Networkmanager check: "),
|
||||
}
|
||||
|
||||
# MSGC = Strings on Cards
|
||||
MSGC = {
|
||||
"checking": _("Checking..."),
|
||||
"version_check": _("Version: Checking..."),
|
||||
"latest": _("Latest: "),
|
||||
"update_available": _("Update available "),
|
||||
"update_on": _("Update on "),
|
||||
"available_lower": _("available"),
|
||||
"up_to_date": _("Up to date"),
|
||||
"latest_unknown": _("Latest unknown"),
|
||||
"could_not_check": _("Could not check latest version"),
|
||||
@ -873,7 +905,7 @@ class LocaleStrings:
|
||||
|
||||
# MSGP = Others print strings
|
||||
MSGP = {
|
||||
"tk_install": _("Installing tkinter for )"),
|
||||
"tk_install": _("Installing tkinter for "),
|
||||
"command_string": _("Command: "),
|
||||
"tk_success": _("TKinter installation completed successfully!"),
|
||||
"tk_failed": _("TKinter installation failed: "),
|
||||
@ -888,3 +920,52 @@ class LocaleStrings:
|
||||
"final_result": _(" Final result: "),
|
||||
"get_version_error": _("Error getting version for "),
|
||||
}
|
||||
|
||||
# MSGG = String on AppImageMessagDialogs and Strings
|
||||
MSGA = {
|
||||
"gitea_gpg_error": _("Error verifying signature: "),
|
||||
"not_gpg_found": _("Could not find GPG signature: "),
|
||||
"SHA256_File_not_found": _("SHA256-File not found: "),
|
||||
"SHA256_File_not_found1": _("Would you like to continue?"),
|
||||
"SHA256_hash mismatch": _("SHA256 hash mismatch. File might be corrupted!"),
|
||||
"Failed_retrieving": _("Failed to retrieve version from Gitea API"),
|
||||
"Error_retrieving": _("Error retrieving latest version: "),
|
||||
"gpg_verify_success": _("GPG verification successful. Signature is valid."),
|
||||
"error_gpg_check": _("Error GPG check: "),
|
||||
"error_gpg_download": _("Error downloading or verifying AppImage: "),
|
||||
"error_repo": _("Error accessing Gitea repository: "),
|
||||
"error": _("Error: "),
|
||||
"appimage_renamed": _("The AppImage renamed..."),
|
||||
"appimage_rename_error": _("Error renaming the AppImage: "),
|
||||
"appimage_executable_error": _("Error making the AppImage executable: "),
|
||||
"appimage_executable_success": _("The AppImage has been made executable"),
|
||||
"appimage_not_exist": _("Error: The AppImage file does not exist."),
|
||||
}
|
||||
|
||||
MSGGPG = {
|
||||
"gpg_missing": _(
|
||||
"Warning: 'gpg' is not installed. Please install it to verify the AppImage signature."
|
||||
),
|
||||
"url_not_reachable": _("URL not reachable: "),
|
||||
"corrupted_file": _(
|
||||
"No fingerprint found in the key. File might be corrupted or empty."
|
||||
),
|
||||
"mismatch": _("Fingerprint mismatch: Expected "),
|
||||
"but_got": _("but got: "),
|
||||
"failed_import": _("Failed to import public key: "),
|
||||
"fingerprint_extract": _("GPG fingerprint extraction failed: "),
|
||||
"error_import_key": _("Error importing GPG key from "),
|
||||
"error_updating_trust_level": _("Error updating trust level: "),
|
||||
"error_executing_script": _("Error executing script to update trust level: "),
|
||||
"keyserver_reachable": _(
|
||||
"OpenPGP keyserver is reachable. Proceeding with download."
|
||||
),
|
||||
"keyserver_unreachable": _(
|
||||
"OpenPGP keyserver unreachable. Skipping this source."
|
||||
),
|
||||
"all_keys_valid": _(
|
||||
"All keys have valid fingerprints matching the expected value."
|
||||
),
|
||||
"set_trust_level": _("Trust level 5 successfully applied to key "),
|
||||
"not_all_keys_are_valid": _("Not all keys are valid."),
|
||||
}
|
||||
|
430
network.py
430
network.py
@ -1,28 +1,428 @@
|
||||
import socket
|
||||
import os
|
||||
import urllib.request
|
||||
import json
|
||||
import hashlib
|
||||
import subprocess
|
||||
from typing import Union, List
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
|
||||
class GiteaUpdate:
|
||||
class GPGManager:
|
||||
|
||||
@staticmethod
|
||||
def api_down(url, current_version=""):
|
||||
"""Get latest version from Gitea API"""
|
||||
def get_gpg() -> bool:
|
||||
"""
|
||||
Check if gpg is installed.
|
||||
|
||||
Returns:
|
||||
bool: True if `gpg` is installed, False otherwise.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["which gpg || command -v gpg"],
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_key_already_imported(key_id: str) -> bool:
|
||||
"""
|
||||
Prüft, ob der Schlüssel bereits im Keyring importiert ist.
|
||||
|
||||
Args:
|
||||
key_id (str): ID des öffentlichen Schlüssels (z. B. '7D8A6E1F9B4C3A5D...')
|
||||
|
||||
Returns:
|
||||
bool: True, wenn der Schlüssel vorhanden ist.
|
||||
"""
|
||||
from message import MessageDialog
|
||||
from manager import LocaleStrings
|
||||
|
||||
# Check if `gpg` is installed
|
||||
if not GPGManager.get_gpg():
|
||||
|
||||
MessageDialog("warning", LocaleStrings.MSGGPG["gpg_missing"]).show()
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--list-keys", key_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if key_id in result.stdout:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
if e.returncode == 2:
|
||||
return False
|
||||
else:
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGA['error_gpg_check']}{e}"
|
||||
).show()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_url_reachable(url: str, timeout: int = 5) -> bool:
|
||||
"""
|
||||
Checks if a given URL is reachable.
|
||||
|
||||
Args:
|
||||
url (str): The URL to check.
|
||||
timeout (int): Timeout in seconds for the connection attempt.
|
||||
|
||||
Returns:
|
||||
bool: True if the URL is reachable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
urllib.request.urlopen(url, timeout=timeout)
|
||||
return True
|
||||
except Exception as e:
|
||||
from message import MessageDialog
|
||||
from manager import LocaleStrings
|
||||
|
||||
MessageDialog(
|
||||
"error",
|
||||
f"{LocaleStrings.MSGGPG['url_not_reachable']}{url} - {LocaleStrings.MSGA['error']}{e}",
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def import_key_from_url(
|
||||
key_url: str, expected_fingerprint: str, filename: str
|
||||
) -> bool:
|
||||
"""
|
||||
Downloads a GPG public key from the given URL, verifies its fingerprint matches
|
||||
the expected value, and imports it into the local GPG keyring.
|
||||
|
||||
Args:
|
||||
key_url (str): URL to the `.asc` public key file.
|
||||
expected_fingerprint (str): Expected 40-character hexadecimal fingerprint of the key.
|
||||
filename (str): Destination filename for saving the downloaded key in `PUBLIC_KEYS_DIR`.
|
||||
|
||||
Returns:
|
||||
bool: True if the key was downloaded, verified, and successfully imported. False otherwise.
|
||||
"""
|
||||
from message import MessageDialog
|
||||
from manager import LocaleStrings
|
||||
|
||||
# Configuration: Define the directory for storing public key files
|
||||
PUBLIC_KEYS_DIR = "/tmp/public_keys"
|
||||
if not os.path.exists(PUBLIC_KEYS_DIR):
|
||||
os.makedirs(PUBLIC_KEYS_DIR)
|
||||
|
||||
try:
|
||||
# Construct full path to save the key file
|
||||
key_path = os.path.join(PUBLIC_KEYS_DIR, filename)
|
||||
print(f"Downloading public key from {key_url} to {key_path}")
|
||||
urllib.request.urlretrieve(key_url, key_path)
|
||||
|
||||
result = subprocess.run(
|
||||
["gpg", "-fingerprint", key_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Extract all fingerprints from GPG output (40-character hexadecimal)
|
||||
fingerprints_from_key: List[str] = []
|
||||
for line in result.stdout.splitlines():
|
||||
matches = re.findall(r"\b[0-9A-Fa-f]{40}\b", line)
|
||||
fingerprints_from_key.extend(matches)
|
||||
|
||||
if not fingerprints_from_key:
|
||||
MessageDialog(
|
||||
"warning", LocaleStrings.MSGGPG["corrupted_file"]
|
||||
).show()
|
||||
return False
|
||||
|
||||
# Check if any of the extracted fingerprints match the expected one
|
||||
found = any(fp == expected_fingerprint for fp in fingerprints_from_key)
|
||||
if not found:
|
||||
MessageDialog(
|
||||
"warning",
|
||||
f"{LocaleStrings.MSGGPG['mismatch']}\n{expected_fingerprint}\n{LocaleStrings.MSGGPG['but_got']}\n{fingerprints_from_key}",
|
||||
wraplength=450,
|
||||
).show()
|
||||
|
||||
return False
|
||||
|
||||
else:
|
||||
# Import key into GPG keyring
|
||||
result_import = subprocess.run(
|
||||
["gpg", "--import", key_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if result_import.returncode == 0:
|
||||
print(f"Public key from {key_url} successfully imported.")
|
||||
return True
|
||||
else:
|
||||
MessageDialog(
|
||||
"error",
|
||||
f"{LocaleStrings.MSGGPG['failed_import']}{result_import.stderr}",
|
||||
).show()
|
||||
return False
|
||||
|
||||
else:
|
||||
MessageDialog(
|
||||
"error",
|
||||
f"{LocaleStrings.MSGGPG['fingerprint_extract']}{result.stderr}",
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGGPG['error_import_key']}{key_url}: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def update_gpg_trust_level(key_id: str, trust_level: int = 5) -> bool:
|
||||
"""
|
||||
Sets the trust level for a specified GPG public key.
|
||||
|
||||
Args:
|
||||
key_id (str): The hexadecimal ID of the key to update.
|
||||
trust_level (int): Trust level (1-5). Default is 5 (fully trusted).
|
||||
|
||||
Returns:
|
||||
bool: True if the trust level was successfully updated, False otherwise.
|
||||
"""
|
||||
script = f"""#!/bin/bash
|
||||
# Set required environment variables
|
||||
export GPG_TTY=$(tty)
|
||||
export GNUPG_STATUS="1"
|
||||
export GNUPGAGENT_INFO_FILE="/dev/null"
|
||||
gpg --batch \
|
||||
--no-tty \
|
||||
--command-fd 0 \
|
||||
--edit-key {key_id} << EOF
|
||||
trust
|
||||
{trust_level}
|
||||
quit
|
||||
EOF
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
from message import MessageDialog
|
||||
from manager import LocaleStrings
|
||||
|
||||
# Create temporary script file
|
||||
temp_script = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False, encoding="utf-8"
|
||||
)
|
||||
temp_script.write(script)
|
||||
temp_script.close()
|
||||
|
||||
print(f"Setting trust level {trust_level} for key {key_id}")
|
||||
result = subprocess.run(
|
||||
["bash", temp_script.name], capture_output=True, text=True, check=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
|
||||
MessageDialog(
|
||||
"error",
|
||||
f"{LocaleStrings.MSGGPG['error_updating_trust_level']}{result.stderr}",
|
||||
).show()
|
||||
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGGPG['error_executing_script']}{e}"
|
||||
).show()
|
||||
return False
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.unlink(temp_script.name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GiteaUpdater:
|
||||
@staticmethod
|
||||
def get_latest_version_from_api(url: str, current_version: str = "") -> str:
|
||||
"""
|
||||
Fetches the latest version of a project from the Gitea API.
|
||||
|
||||
Args:
|
||||
url (str): The URL to query the Gitea API for releases.
|
||||
current_version (str, optional): Not used in this implementation. Defaults to "".
|
||||
|
||||
Returns:
|
||||
str: Latest version tag name without the 'v' prefix.
|
||||
If an error occurs, returns "Unknown".
|
||||
"""
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
if data and len(data) > 0:
|
||||
latest_version = data[0].get("tag_name", "Unknown")
|
||||
return latest_version.lstrip("v") # Remove 'v' prefix if present
|
||||
return latest_version.lstrip("v") # Remove 'v' prefix
|
||||
return "Unknown"
|
||||
except Exception as e:
|
||||
print(f"API Error: {e}")
|
||||
return "Unknown"
|
||||
|
||||
@staticmethod
|
||||
def download_appimage(
|
||||
version: str = "latest",
|
||||
verify_checksum: bool = False,
|
||||
verify_signature: bool = False,
|
||||
) -> Union[str, None]:
|
||||
"""
|
||||
Downloads the AppImage file to /tmp/portinstaller and performs verification checks.
|
||||
|
||||
Args:
|
||||
version (str): 'latest' for the latest version or a specific version.
|
||||
verify_checksum (bool): Whether to perform SHA256 checksum verification.
|
||||
verify_signature (bool): Whether to validate GPG signature.
|
||||
|
||||
Returns:
|
||||
str: Full path to the AppImage if all checks pass. Otherwise, returns an error message.
|
||||
None: If an exception occurs during download or verification.
|
||||
"""
|
||||
base_url = "https://git.ilunix.de/punix/lxtools_installer/releases/download/"
|
||||
|
||||
from message import MessageDialog
|
||||
from manager import LocaleStrings
|
||||
|
||||
# Get latest version from API if not provided
|
||||
if version == "latest":
|
||||
try:
|
||||
|
||||
with urllib.request.urlopen(
|
||||
"https://git.ilunix.de/api/v1/repos/punix/lxtools_installer/releases?limit=1",
|
||||
timeout=10,
|
||||
) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
latest_version = data[0].get("tag_name")
|
||||
if not latest_version:
|
||||
print(LocaleStrings.MSGA["Failed_retrieving"])
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"{LocaleStrings.MSGA['Error_retrieving']}{e}")
|
||||
return None
|
||||
|
||||
else:
|
||||
latest_version = version
|
||||
|
||||
# Create /tmp/portinstaller directory if it doesn't exist
|
||||
download_dir = "/tmp/portinstaller"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
filename = f"{download_dir}/lxtools_installer{latest_version}-x86_64.AppImage"
|
||||
appimage_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}"
|
||||
checksum_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}.sha256"
|
||||
signature_url = f"{base_url}{latest_version}/{filename.split('/')[-1]}.asc"
|
||||
|
||||
try:
|
||||
print("Downloading AppImage Updater...")
|
||||
urllib.request.urlretrieve(appimage_url, filename)
|
||||
|
||||
# SHA256 checksum verification
|
||||
result = None
|
||||
if verify_checksum:
|
||||
try:
|
||||
with urllib.request.urlopen(checksum_url, timeout=10) as response:
|
||||
checksum_content = response.read().decode()
|
||||
expected_hash, _ = checksum_content.strip().split(" ")
|
||||
except Exception as e:
|
||||
|
||||
result = MessageDialog(
|
||||
"ask",
|
||||
f"{LocaleStrings.MSGA['SHA256_File_not_found']}{e} {LocaleStrings.MSGA['SHA256_File_not_found1']}",
|
||||
buttons=["Yes", "No"],
|
||||
).show()
|
||||
|
||||
if not result:
|
||||
return "Checksum file not found"
|
||||
|
||||
if result:
|
||||
pass
|
||||
else:
|
||||
with open(filename, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
if expected_hash != file_hash:
|
||||
|
||||
MessageDialog(
|
||||
"warning", LocaleStrings.MSGA["SHA256 hash mismatch"]
|
||||
).show()
|
||||
return "Checksum mismatch"
|
||||
|
||||
# GPG signature verification
|
||||
if verify_signature:
|
||||
signature_path = f"{download_dir}/{filename.split('/')[-1]}.asc"
|
||||
try:
|
||||
urllib.request.urlretrieve(signature_url, signature_path)
|
||||
except Exception as e:
|
||||
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGA['not_gpg_found']}{e}"
|
||||
).show()
|
||||
return "Signature file not found"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--verify", signature_path, filename],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(LocaleStrings.MSGA["gpg_verify_success"])
|
||||
except Exception as e:
|
||||
from message import MessageDialog
|
||||
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGA['error_gpg_check']}{e.stderr}"
|
||||
).show()
|
||||
return "Signature verification failed"
|
||||
|
||||
# Return the full path to the AppImage
|
||||
return filename
|
||||
|
||||
except Exception as e:
|
||||
from message import MessageDialog
|
||||
|
||||
MessageDialog(
|
||||
"error", f"{LocaleStrings.MSGA['error_gpg_download']}{e}"
|
||||
).show()
|
||||
return None
|
||||
|
||||
|
||||
class NetworkChecker:
|
||||
@staticmethod
|
||||
def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
|
||||
"""Check if internet connection is available"""
|
||||
def check_internet_connection(
|
||||
host: str = "8.8.8.8", port: int = 53, timeout: float = 3
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if an internet connection is available.
|
||||
|
||||
Args:
|
||||
host (str): Host to connect to for testing. Defaults to "8.8.8.8" (Google DNS).
|
||||
port (int): Port number to use for the test. Defaults to 53.
|
||||
timeout (float): Timeout in seconds for the connection attempt. Defaults to 3.
|
||||
|
||||
Returns:
|
||||
bool: True if internet is available, False otherwise.
|
||||
"""
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
|
||||
@ -31,10 +431,22 @@ class NetworkChecker:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def check_repository_access(url="https://git.ilunix.de", timeout=5):
|
||||
"""Check if repository is accessible"""
|
||||
def check_repository_access(
|
||||
url: str = "https://git.ilunix.de", timeout: float = 5
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the Gitea repository is accessible.
|
||||
|
||||
Args:
|
||||
url (str): The URL of the Gitea repository. Defaults to "https://git.ilunix.de".
|
||||
timeout (float): Timeout in seconds for the connection attempt. Defaults to 5.
|
||||
|
||||
Returns:
|
||||
bool: True if the repository is accessible, False otherwise.
|
||||
"""
|
||||
try:
|
||||
urllib.request.urlopen(url, timeout=timeout)
|
||||
return True
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Error accessing Gitea repository: {e}")
|
||||
return False
|
||||
|
64
public_key.asc
Normal file
64
public_key.asc
Normal file
@ -0,0 +1,64 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
40WrGrMw8RQJRui1bNFIz4BT0w==
|
||||
=G2Bo
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
Reference in New Issue
Block a user