Compare commits
24 Commits
5740b22583
...
main
Author | SHA1 | Date | |
---|---|---|---|
ff97ed5b20 | |||
fbba0028ab | |||
9873a293f7 | |||
29f237ab42 | |||
5986ce3b48 | |||
bc1ec45ecd | |||
4006b917f9 | |||
9db3aff611 | |||
aa8923ca47 | |||
ac3a375357 | |||
c500b4f1ea | |||
80a7018a72 | |||
cfebeafd9b | |||
299404eaac | |||
be43e50065 | |||
44e75fa1b0 | |||
f416f66ee1 | |||
12904e843c | |||
e824094556 | |||
ed269af1d2 | |||
01bd6ab263 | |||
a63d54f128 | |||
3ba041a28e | |||
a9b6fccbf7 |
32
.gitignore
vendored
32
.gitignore
vendored
@@ -4,3 +4,35 @@ debug.log
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
|
# Build-Artefakte
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
certs/
|
||||||
|
*.spec.bak
|
||||||
|
lxtools_installer
|
||||||
|
lxtools_installer.AppImage
|
||||||
|
lxtools_installer_compat
|
||||||
|
build_compatible.sh
|
||||||
|
build_local.sh
|
||||||
|
clean_build.sh
|
||||||
|
start_builder.sh
|
||||||
|
test_extract.py
|
||||||
|
test_paths.py
|
||||||
|
test_resources.py
|
||||||
|
manager_fixed.py
|
||||||
|
test_simple.sh
|
||||||
|
test_container.sh
|
||||||
|
gpg_setup.sh
|
||||||
|
gpg_simple_setup.sh
|
||||||
|
lxtools_installer.spec
|
||||||
|
lxtoolsinstaller.pot
|
||||||
|
|
||||||
|
# Docker-Build
|
||||||
|
docker_build/
|
||||||
|
debug_docker.sh
|
||||||
|
Dockerfile.nuitka
|
||||||
|
DOCKER_BUILD_ANLEITUNG.md
|
||||||
|
nuitka_builder.py
|
||||||
|
Dockerfile.test
|
||||||
|
Dockerfile.simple
|
||||||
|
82
Changelog
82
Changelog
@@ -2,7 +2,85 @@ Changelog for LXTools installer
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
- replace pack with grid
|
- 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
|
||||||
|
29.06.2025
|
||||||
|
|
||||||
|
- add methode sigi, clean_files and remove_lxtools_files
|
||||||
|
for remove files and dirs on close lxtools_installer
|
||||||
|
|
||||||
|
- fix message dialog on font and padding
|
||||||
|
|
||||||
|
- add methods check polkit and check Networkmanager is installed
|
||||||
|
and view in header is result false
|
||||||
|
|
||||||
|
|
||||||
|
### Added
|
||||||
|
23-06-2025
|
||||||
|
|
||||||
|
- Add unzip check, requests check, wget check for Arch Linux
|
||||||
|
|
||||||
|
- fix remove config dirs and logfile and ssl privatkey on uninstall
|
||||||
|
|
||||||
|
- method the number of users of the system is extended
|
||||||
|
for information message when a program is uninstalled
|
||||||
|
and there are more than one user on the system.
|
||||||
|
|
||||||
|
|
||||||
|
### Added
|
||||||
|
22-06-2025
|
||||||
|
|
||||||
|
- ssl certificate integrate in Appinstaller
|
||||||
|
|
||||||
|
- Installer now takes into account the current python
|
||||||
|
version ud the respective recognized system to run with
|
||||||
|
it on all supported systems.
|
||||||
|
|
||||||
|
|
||||||
|
### Added
|
||||||
|
21-06-2025
|
||||||
|
|
||||||
|
- extract now files needed in the work directory Theme, Icons and translation
|
||||||
|
|
||||||
|
- if python is not found, it displays this in red in the header,
|
||||||
|
with new get_python_version method
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
18-06-2025
|
18-06-2025
|
||||||
@@ -15,6 +93,7 @@ Changelog for LXTools installer
|
|||||||
|
|
||||||
- Installer divided into several modules and added new MessageDialog module
|
- Installer divided into several modules and added new MessageDialog module
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
4-06-2025
|
4-06-2025
|
||||||
|
|
||||||
@@ -23,6 +102,7 @@ Changelog for LXTools installer
|
|||||||
|
|
||||||
- add ensure_shared_libs_pth_exists Script to install
|
- add ensure_shared_libs_pth_exists Script to install
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
4-06-2025
|
4-06-2025
|
||||||
|
|
||||||
|
20
README.md
20
README.md
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
LX Tools Installer is a GUI for simple install, update, and remove Apps from ilunix.de
|
LX Tools Installer is a GUI for simple install, update, and remove Apps from ilunix.de
|
||||||
|
|
||||||
|
# Fingerprint
|
||||||
|
|
||||||
|
743745087C6414E00F1EF84D4CCF06B6CE2A4C7F
|
||||||
|
|
||||||
|
add to your gpg keyring:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://git.ilunix.de/punix/lxtools_installer/raw/branch/main/public_key.asc -O - | gpg --import
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
wget https://keys.openpgp.org/vks/v1/by-fingerprint/743745087C6414E00F1EF84D4CCF06B6CE2A4C7F -O - | gpg --import
|
||||||
|
```
|
||||||
|
The Appimage automatically checks whether the public_key has already been imported,
|
||||||
|
and if not it is downloaded from both sources and only imported when all the keys match.
|
||||||
|
|
||||||
|
This is to ensure that no manipulated software is used.
|
||||||
|
|
||||||
|
# Not currently supported
|
||||||
|
- Open Suse Tumbleweed and Leap (Let's get back)
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
[](https://fb.ilunix.de/share/ZnfG9gxv)
|
[](https://fb.ilunix.de/share/ZnfG9gxv)
|
@@ -1,68 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# ✅ Path to be added in the .pth file
|
|
||||||
SHARED_LIBS_PATH = "/usr/local/share/shared_libs"
|
|
||||||
PTH_FILE_NAME = "shared_libs.pth"
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_shared_libs_pth_exists():
|
|
||||||
"""
|
|
||||||
Checks if all site-packages directories have a `.pth` file with the correct path.
|
|
||||||
Creates or updates it if missing or incorrect.
|
|
||||||
"""
|
|
||||||
# Search for all site-packages directories (e.g., /usr/lib/python3.x/site-packages/)
|
|
||||||
for root, dirs, files in os.walk("/usr"):
|
|
||||||
if "site-packages" in dirs:
|
|
||||||
site_packages_dir = os.path.join(root, "site-packages")
|
|
||||||
|
|
||||||
pth_file_path = os.path.join(site_packages_dir, PTH_FILE_NAME)
|
|
||||||
|
|
||||||
# Check if the file exists and is correct
|
|
||||||
if not os.path.exists(pth_file_path):
|
|
||||||
print(f"⚠️ .pth file not found: {pth_file_path}. Creating...")
|
|
||||||
with open(pth_file_path, "w") as f:
|
|
||||||
f.write(SHARED_LIBS_PATH + "\n")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Check if the correct path is in the file
|
|
||||||
with open(pth_file_path, "r") as f:
|
|
||||||
content = f.read().strip()
|
|
||||||
|
|
||||||
if not content == SHARED_LIBS_PATH:
|
|
||||||
print(f"⚠️ .pth file exists but has incorrect content. Fixing...")
|
|
||||||
with open(pth_file_path, "w") as f:
|
|
||||||
f.write(SHARED_LIBS_PATH + "\n")
|
|
||||||
|
|
||||||
print("✅ All .pth files checked and corrected.")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
# Try to import the module
|
|
||||||
from shared_libs.wp_app_config import AppConfig
|
|
||||||
|
|
||||||
print("✅ 'shared_libs' is correctly loaded. Starting the application...")
|
|
||||||
|
|
||||||
# Your main program logic here...
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
# Only handle errors related to missing .pth file
|
|
||||||
if "No module named 'shared_libs'" in str(e):
|
|
||||||
print("⚠️ Error: 'shared_libs' module not found. Checking .pth file...")
|
|
||||||
ensure_shared_libs_pth_exists()
|
|
||||||
|
|
||||||
# Try again after fixing the .pth file
|
|
||||||
try:
|
|
||||||
from shared_libs.wp_app_config import AppConfig
|
|
||||||
|
|
||||||
print("✅ After correcting the .pth file: Module loaded.")
|
|
||||||
# Your main program logic here...
|
|
||||||
except Exception as e2:
|
|
||||||
print(f"❌ Error after correcting the .pth file: {e2}")
|
|
||||||
else:
|
|
||||||
# For other errors, re-raise them
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
Binary file not shown.
BIN
lx-icons/16/settings.png
Normal file
BIN
lx-icons/16/settings.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 757 B |
BIN
lx-icons/16/wg_vpn.png
Normal file
BIN
lx-icons/16/wg_vpn.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 846 B |
BIN
lx-icons/32/lxtools_key.png
Normal file
BIN
lx-icons/32/lxtools_key.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.3 KiB |
File diff suppressed because it is too large
Load Diff
878
manager.py
878
manager.py
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@ class MessageDialog:
|
|||||||
self.window = tk.Toplevel(master)
|
self.window = tk.Toplevel(master)
|
||||||
self.window.grab_set()
|
self.window.grab_set()
|
||||||
self.window.resizable(False, False)
|
self.window.resizable(False, False)
|
||||||
ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5)
|
ttk.Style().configure("TButton")
|
||||||
self.buttons_widgets = []
|
self.buttons_widgets = []
|
||||||
self.current_button_index = 0
|
self.current_button_index = 0
|
||||||
self._load_icons()
|
self._load_icons()
|
||||||
|
430
network.py
430
network.py
@@ -1,28 +1,428 @@
|
|||||||
import socket
|
import socket
|
||||||
|
import os
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
|
import subprocess
|
||||||
|
from typing import Union, List
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
class GiteaUpdate:
|
class GPGManager:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def api_down(url, current_version=""):
|
def get_gpg() -> bool:
|
||||||
"""Get latest version from Gitea API"""
|
"""
|
||||||
|
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:
|
try:
|
||||||
with urllib.request.urlopen(url, timeout=10) as response:
|
with urllib.request.urlopen(url, timeout=10) as response:
|
||||||
data = json.loads(response.read().decode())
|
data = json.loads(response.read().decode())
|
||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
latest_version = data[0].get("tag_name", "Unknown")
|
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"
|
return "Unknown"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"API Error: {e}")
|
print(f"API Error: {e}")
|
||||||
return "Unknown"
|
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:
|
class NetworkChecker:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
|
def check_internet_connection(
|
||||||
"""Check if internet connection is available"""
|
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:
|
try:
|
||||||
socket.setdefaulttimeout(timeout)
|
socket.setdefaulttimeout(timeout)
|
||||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
|
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
|
||||||
@@ -31,10 +431,22 @@ class NetworkChecker:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_repository_access(url="https://git.ilunix.de", timeout=5):
|
def check_repository_access(
|
||||||
"""Check if repository is accessible"""
|
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:
|
try:
|
||||||
urllib.request.urlopen(url, timeout=timeout)
|
urllib.request.urlopen(url, timeout=timeout)
|
||||||
return True
|
return True
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(f"Error accessing Gitea repository: {e}")
|
||||||
return False
|
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