ISO Downloader with native GUI and checksum verification

This commit is contained in:
Tronax 2026-06-13 10:55:39 +02:00
commit 3038fcf94c
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
12 changed files with 1470 additions and 0 deletions

47
.gitignore vendored Normal file
View file

@ -0,0 +1,47 @@
# Build-Verzeichnisse
/build/
/build-*/
cmake-build-*/
# CMake-Artefakte
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
CTestTestfile.cmake
Makefile
*.cmake
!CMakeLists.txt
# Qt / MOC / UIC / RCC
moc_*.cpp
moc_*.h
*_autogen/
qrc_*.cpp
ui_*.h
*.qmake.stash
.qmake.cache
# Kompilate
*.o
*.obj
*.so
*.a
*.dll
*.dylib
*.exe
glorious-iso-dl
# Heruntergeladene ISOs und unfertige Downloads
*.iso
*.img
*.part
# Editor / IDE
.idea/
.vscode/
*.user
*.swp
*~
.DS_Store
compile_commands.json
.claude/

22
CMakeLists.txt Normal file
View file

@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.21)
project(glorious-iso-dl VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets Network)
qt_add_executable(glorious-iso-dl
src/main.cpp
src/Distro.h
src/DistroRegistry.cpp
src/Resolver.h
src/Resolver.cpp
src/DownloadManager.h
src/DownloadManager.cpp
src/MainWindow.h
src/MainWindow.cpp
)
target_link_libraries(glorious-iso-dl PRIVATE Qt6::Widgets Qt6::Network)

91
README.md Normal file
View file

@ -0,0 +1,91 @@
# Glorious ISO Downloader
Ein grafischer ISO-Downloader für Linux-Distributionen auf Basis von **C++ und Qt6**.
Das Programm ermittelt automatisch den aktuellen Download-Link der gewählten
Distribution und **verifiziert die heruntergeladene Datei anhand ihrer Prüfsumme**.
## Funktionen
- **Automatische Link-Auflösung** — keine manuelle Suche nach Mirrors oder
Versionsnummern. Das Programm liest Release-Seiten und Verzeichnislisten aus und
ermittelt selbstständig die neueste Version.
- **Prüfsummen-Verifikation während des Downloads** — der Hash wird im Stream
berechnet (MD5, SHA-1, SHA-256, SHA-512). Stimmt er nicht mit der offiziellen
Prüfsumme überein, wird die Datei **automatisch verworfen**.
- **50 unterstützte Distributionen**, gruppiert nach Familie (siehe unten).
- **Sicherheitshinweise** in der Oberfläche:
- Warnung bei unverschlüsselten HTTP-Quellen (verhindert keine, aber meldet
Manipulationsrisiken; HTTPS→HTTP-Downgrades bei Redirects werden blockiert).
- Warnung, wenn ein Projekt nur schwache Hashes (MD5/SHA-1) anbietet.
- Warnung, wenn überhaupt keine Prüfsumme verfügbar ist.
- **Versions-Fallback** — ist die scheinbar neueste Version noch nicht vollständig
veröffentlicht, probiert das Programm automatisch die nächstniedrigere.
## Unterstützte Distributionen
| Familie | Distributionen |
|---|---|
| **Arch** | Arch Linux, Manjaro KDE, CachyOS, EndeavourOS, Artix, Garuda Dr460nized, ArcoLinux¹, BlackArch |
| **Debian** | Debian, Kali, ParrotOS, Tails, Devuan, antiX, MX Linux |
| **Ubuntu** | Ubuntu, Kubuntu, Xubuntu, Lubuntu, Ubuntu MATE, Mint, Pop!_OS, KDE Neon, Linux Lite, Trisquel |
| **Red Hat / Fedora** | Fedora, Rocky, AlmaLinux, CentOS Stream, Qubes OS |
| **SUSE / Slackware / Mandriva** | openSUSE Tumbleweed, Slackware, Mageia, PCLinuxOS |
| **Unabhängig & Spezial** | Void, NixOS, Gentoo, Alpine, Deepin, Clear Linux¹, Puppy, TinyCore, GoboLinux¹, KaOS, Calculate Linux, Grml, SystemRescue, Proxmox VE, TrueNAS SCALE, Proxmox Backup Server |
¹ *ArcoLinux* und *Clear Linux* wurden 2025 eingestellt, *GoboLinux* ist seit 2020
inaktiv. Die Einträge sind enthalten, aber entsprechend markiert — die automatische
Auflösung kann fehlschlagen, da Mirrors abgeschaltet wurden.
## Sicherheitshinweis
Eine Prüfsummen-Verifikation schützt vor **defekten oder veränderten Downloads**, aber
nicht vor einer Kompromittierung der Bezugsquelle selbst. Für besonders
sicherheitskritische Images (z. B. Tails, Qubes OS, Trisquel) solltest du zusätzlich
die **GPG-Signatur** der Prüfsummendatei mit dem offiziellen Schlüssel des Projekts
prüfen. Das Programm weist in diesen Fällen darauf hin.
## Bauen
Voraussetzungen: **Qt 6** (Widgets, Network), **CMake ≥ 3.21**, ein C++20-Compiler.
Unter Arch Linux:
```bash
sudo pacman -S qt6-base cmake gcc
```
Bauen:
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
```
## Benutzung
```bash
./build/glorious-iso-dl
```
1. Distribution im Baum links auswählen.
2. **„Aktuellen Download-Link ermitteln"** klicken — Version, URL und erwartete
Prüfsumme werden angezeigt.
3. Zielordner wählen und **„Herunterladen & verifizieren"** klicken.
4. Nach Abschluss meldet das Programm, ob die Prüfsumme stimmt.
## Projektstruktur
| Datei | Inhalt |
|---|---|
| `src/Distro.h` | Datenmodell für die deklarativen Auflösungs-Rezepte |
| `src/DistroRegistry.cpp` | Definitionen aller unterstützten Distributionen |
| `src/Resolver.{h,cpp}` | Dreistufige Link-Auflösung (Version → Seite → Prüfsumme) |
| `src/DownloadManager.{h,cpp}` | Download mit Streaming-Hashprüfung |
| `src/MainWindow.{h,cpp}` | Grafische Oberfläche |
| `src/main.cpp` | Einstiegspunkt |
## Wartung
Ändert ein Projekt sein URL-Schema oder seine Verzeichnisstruktur, muss der
betreffende Eintrag in `src/DistroRegistry.cpp` angepasst werden. Jeder Eintrag
ist deklarativ aufgebaut; das Format ist in `src/Distro.h` dokumentiert.

47
src/Distro.h Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include <QString>
#include <QList>
#include <QCryptographicHash>
// Beschreibt, wie der aktuelle ISO-Link einer Distribution aufgelöst wird.
//
// Auflösung in bis zu 3 Schritten (alle optional, Platzhalter werden ersetzt):
// 1. versionUrl + versionRegex : Seite laden, alle Treffer (Capture-Gruppe 1)
// sammeln, höchste Version gewinnt -> %VERSION%
// 2. pageUrl + linkRegex : Seite laden, ISO-Link/Dateiname per Regex
// finden (höchster Treffer gewinnt), relativ zur pageUrl aufgelöst.
// hashRegex (optional) : Hash direkt aus derselben Seite ziehen
// checksumLinkRegex (opt.) : Link zur Prüfsummendatei aus der Seite ziehen
// 3. checksumUrl : Prüfsummendatei laden.
// Mit isoPattern + downloadBase: Dateiname wird AUS der Prüfsummendatei
// ermittelt (Listenmodus). Ohne isoPattern: Hash zum bereits bekannten
// Dateinamen suchen (oder einzelner Hash in der Datei).
//
// Platzhalter: %VERSION%, %ISOURL%, %FILENAME%
struct Distro {
QString id;
QString name;
QString family;
QString notes;
QString versionUrl;
QString versionRegex;
QString pageUrl;
QString linkRegex;
QString checksumLinkRegex;
QString hashRegex;
QString isoUrl; // direktes URL-Template (falls kein linkRegex/isoPattern)
QString checksumUrl;
QString isoPattern; // Listenmodus: Dateiname-Regex in der Prüfsummendatei
QString downloadBase; // Listenmodus: Basis-URL für den Download
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256;
};
namespace DistroRegistry {
const QList<Distro>& all();
}

383
src/DistroRegistry.cpp Normal file
View file

@ -0,0 +1,383 @@
#include "Distro.h"
// Hinweis: Die Auflösung basiert auf offiziellen Mirrors/Release-Seiten.
// Ändert ein Projekt sein URL-Schema, muss der Eintrag angepasst werden.
const QList<Distro>& DistroRegistry::all()
{
static const QList<Distro> list = {
// ───────────────────────── Arch-Familie ─────────────────────────
Distro{
.id = "arch", .name = "Arch Linux", .family = "Arch-Familie",
.checksumUrl = "https://geo.mirror.pkgbuild.com/iso/latest/sha256sums.txt",
.isoPattern = "archlinux-\\d{4}\\.\\d{2}\\.\\d{2}-x86_64\\.iso",
.downloadBase = "https://geo.mirror.pkgbuild.com/iso/latest/",
},
Distro{
.id = "manjaro-kde", .name = "Manjaro KDE", .family = "Arch-Familie",
.pageUrl = "https://manjaro.org/products/download/x86",
.linkRegex = "(https://download\\.manjaro\\.org/kde/[^\"']+?\\.iso)",
.checksumUrl = "%ISOURL%.sha512",
.algo = QCryptographicHash::Sha512,
},
Distro{
.id = "cachyos", .name = "CachyOS (Desktop)", .family = "Arch-Familie",
.versionUrl = "https://mirror.cachyos.org/ISO/desktop/",
.versionRegex = "href=\"(\\d{6,8})/\"",
.isoUrl = "https://mirror.cachyos.org/ISO/desktop/%VERSION%/cachyos-desktop-linux-%VERSION%.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "endeavouros", .name = "EndeavourOS", .family = "Arch-Familie",
.pageUrl = "https://mirror.alpix.eu/endeavouros/iso/",
.linkRegex = "href=\"(EndeavourOS_[A-Za-z-]+-[\\d.\\-]+\\.iso)\"",
.checksumUrl = "%ISOURL%.sha512",
.algo = QCryptographicHash::Sha512,
},
Distro{
.id = "artix", .name = "Artix (Plasma/OpenRC)", .family = "Arch-Familie",
.checksumUrl = "https://download.artixlinux.org/iso/sha256sums",
.isoPattern = "artix-plasma-openrc-[\\w.\\-]+-x86_64\\.iso",
.downloadBase = "https://download.artixlinux.org/iso/",
},
Distro{
.id = "garuda", .name = "Garuda Dr460nized", .family = "Arch-Familie",
.isoUrl = "https://iso.builds.garudalinux.org/iso/latest/garuda/dr460nized/latest.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "arcolinux", .name = "ArcoLinux", .family = "Arch-Familie",
.notes = "Projekt wurde 2025 eingestellt — bekannte Mirrors sind bereits offline, "
"die automatische Auflösung schlägt daher vermutlich fehl.",
.versionUrl = "https://mirror.accum.se/mirror/arcolinux/iso/",
.versionRegex = "href=\"(v[\\d.]+)/\"",
.isoUrl = "https://mirror.accum.se/mirror/arcolinux/iso/%VERSION%/arcolinuxl-%VERSION%-x86_64.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "blackarch", .name = "BlackArch (Slim)", .family = "Arch-Familie",
.notes = "Projekt stellt nur SHA-1 bereit — zusätzlich GPG-Signatur prüfen!",
.pageUrl = "https://blackarch.org/downloads.html",
.linkRegex = "(https://[^\"]+/blackarch-linux-slim-[\\d.]+-x86_64\\.iso)",
.hashRegex = "blackarch-linux-slim-[\\d.]+-x86_64\\.iso[\\s\\S]{0,500}?([a-f0-9]{40})",
.algo = QCryptographicHash::Sha1,
},
// ───────────────────────── Debian-Familie ───────────────────────
Distro{
.id = "debian", .name = "Debian (Netinst)", .family = "Debian-Familie",
.checksumUrl = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS",
.isoPattern = "debian-[\\d.]+-amd64-netinst\\.iso",
.downloadBase = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/",
},
Distro{
.id = "kali", .name = "Kali Linux (Installer)", .family = "Debian-Familie",
.checksumUrl = "https://cdimage.kali.org/current/SHA256SUMS",
.isoPattern = "kali-linux-[\\d.]+[a-z]?-installer-amd64\\.iso",
.downloadBase = "https://cdimage.kali.org/current/",
},
Distro{
.id = "parrot", .name = "ParrotOS Security", .family = "Debian-Familie",
.versionUrl = "https://deb.parrot.sh/parrot/iso/",
.versionRegex = "href=\"(\\d+\\.\\d+(?:\\.\\d+)?)/\"",
.checksumUrl = "https://deb.parrot.sh/parrot/iso/%VERSION%/signed-hashes.txt",
.isoPattern = "Parrot-security-%VERSION%_amd64\\.iso",
.downloadBase = "https://deb.parrot.sh/parrot/iso/%VERSION%/",
},
Distro{
.id = "tails", .name = "Tails", .family = "Debian-Familie",
.pageUrl = "https://tails.net/install/v2/Tails/amd64/stable/latest.json",
.linkRegex = "\"url\":\\s*\"([^\"]+\\.iso)\"",
.hashRegex = "\"sha256\":\\s*\"([0-9a-f]{64})\"[\\s\\S]{0,300}?\\.iso\"",
},
Distro{
.id = "devuan", .name = "Devuan (Netinstall)", .family = "Debian-Familie",
.versionUrl = "https://files.devuan.org/",
.versionRegex = "announce/([a-z]+)-release-announce",
.checksumUrl = "https://files.devuan.org/devuan_%VERSION%/installer-iso/SHA256SUMS.txt",
.isoPattern = "devuan_[a-z]+_[\\d.]+_amd64_netinstall\\.iso",
.downloadBase = "https://files.devuan.org/devuan_%VERSION%/installer-iso/",
},
Distro{
.id = "antix", .name = "antiX (Full)", .family = "Debian-Familie",
.versionUrl = "https://mirrors.dotsrc.org/mx-isos/ANTIX/Final/",
.versionRegex = "href=\"antiX-([\\d.]+)/\"",
.pageUrl = "https://mirrors.dotsrc.org/mx-isos/ANTIX/Final/antiX-%VERSION%/",
.linkRegex = "href=\"(antiX-[\\d.]+_x64-full\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "mxlinux", .name = "MX Linux (Xfce)", .family = "Debian-Familie",
.pageUrl = "https://mirrors.dotsrc.org/mx-isos/MX/Final/Xfce/",
.linkRegex = "href=\"(MX-[\\d.]+_Xfce_x64\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
// ───────────────────────── Ubuntu-Familie ───────────────────────
Distro{
.id = "ubuntu", .name = "Ubuntu Desktop", .family = "Ubuntu-Familie",
.versionUrl = "https://releases.ubuntu.com/",
.versionRegex = "href=\"(\\d\\d\\.\\d\\d(?:\\.\\d+)?)/\"",
.checksumUrl = "https://releases.ubuntu.com/%VERSION%/SHA256SUMS",
.isoPattern = "ubuntu-[\\d.]+-desktop-amd64\\.iso",
.downloadBase = "https://releases.ubuntu.com/%VERSION%/",
},
Distro{
.id = "kubuntu", .name = "Kubuntu", .family = "Ubuntu-Familie",
.versionUrl = "https://cdimage.ubuntu.com/kubuntu/releases/",
.versionRegex = "href=\"(\\d\\d\\.\\d\\d(?:\\.\\d+)?)/\"",
.checksumUrl = "https://cdimage.ubuntu.com/kubuntu/releases/%VERSION%/release/SHA256SUMS",
.isoPattern = "kubuntu-[\\d.]+-desktop-amd64\\.iso",
.downloadBase = "https://cdimage.ubuntu.com/kubuntu/releases/%VERSION%/release/",
},
Distro{
.id = "xubuntu", .name = "Xubuntu", .family = "Ubuntu-Familie",
.versionUrl = "https://cdimage.ubuntu.com/xubuntu/releases/",
.versionRegex = "href=\"(\\d\\d\\.\\d\\d(?:\\.\\d+)?)/\"",
.checksumUrl = "https://cdimage.ubuntu.com/xubuntu/releases/%VERSION%/release/SHA256SUMS",
.isoPattern = "xubuntu-[\\d.]+-desktop-amd64\\.iso",
.downloadBase = "https://cdimage.ubuntu.com/xubuntu/releases/%VERSION%/release/",
},
Distro{
.id = "lubuntu", .name = "Lubuntu", .family = "Ubuntu-Familie",
.versionUrl = "https://cdimage.ubuntu.com/lubuntu/releases/",
.versionRegex = "href=\"(\\d\\d\\.\\d\\d(?:\\.\\d+)?)/\"",
.checksumUrl = "https://cdimage.ubuntu.com/lubuntu/releases/%VERSION%/release/SHA256SUMS",
.isoPattern = "lubuntu-[\\d.]+-desktop-amd64\\.iso",
.downloadBase = "https://cdimage.ubuntu.com/lubuntu/releases/%VERSION%/release/",
},
Distro{
.id = "ubuntu-mate", .name = "Ubuntu MATE", .family = "Ubuntu-Familie",
.versionUrl = "https://cdimage.ubuntu.com/ubuntu-mate/releases/",
.versionRegex = "href=\"(\\d\\d\\.\\d\\d(?:\\.\\d+)?)/\"",
.checksumUrl = "https://cdimage.ubuntu.com/ubuntu-mate/releases/%VERSION%/release/SHA256SUMS",
.isoPattern = "ubuntu-mate-[\\d.]+-desktop-amd64\\.iso",
.downloadBase = "https://cdimage.ubuntu.com/ubuntu-mate/releases/%VERSION%/release/",
},
Distro{
.id = "mint", .name = "Linux Mint (Cinnamon)", .family = "Ubuntu-Familie",
.versionUrl = "https://mirrors.edge.kernel.org/linuxmint/stable/",
.versionRegex = "href=\"(\\d+(?:\\.\\d+)?)/\"",
.checksumUrl = "https://mirrors.edge.kernel.org/linuxmint/stable/%VERSION%/sha256sum.txt",
.isoPattern = "linuxmint-[\\d.]+-cinnamon-64bit\\.iso",
.downloadBase = "https://mirrors.edge.kernel.org/linuxmint/stable/%VERSION%/",
},
Distro{
.id = "popos", .name = "Pop!_OS", .family = "Ubuntu-Familie",
.notes = "Basis-Release (24.04) ist in der API-URL kodiert — bei neuer LTS anpassen.",
.pageUrl = "https://api.pop-os.org/builds/24.04/intel",
.linkRegex = "\"url\":\\s*\"([^\"]+\\.iso)\"",
.hashRegex = "\"sha_sum\":\\s*\"([0-9a-f]{64})\"",
},
Distro{
.id = "kde-neon", .name = "KDE Neon (User)", .family = "Ubuntu-Familie",
.pageUrl = "https://files.kde.org/neon/images/user/current/",
.linkRegex = "href=\"(neon-user-\\d{8}-\\d{4}\\.iso)\"",
.checksumLinkRegex = "href=\"(neon-user-\\d{8}-\\d{4}\\.sha256sum)\"",
},
Distro{
.id = "linux-lite", .name = "Linux Lite", .family = "Ubuntu-Familie",
.versionUrl = "https://repo.linuxliteos.com/linuxlite/isos/",
.versionRegex = "href=\"(\\d+\\.\\d+)/\"",
.isoUrl = "https://repo.linuxliteos.com/linuxlite/isos/%VERSION%/linux-lite-%VERSION%-64bit.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "trisquel", .name = "Trisquel", .family = "Ubuntu-Familie",
.pageUrl = "https://mirror.fsf.org/trisquel-images/",
.linkRegex = "href=\"(trisquel_[\\d.]+_amd64\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
// ─────────────────────── Red Hat / Fedora ───────────────────────
Distro{
.id = "fedora", .name = "Fedora Workstation", .family = "Red Hat / Fedora",
.versionUrl = "https://dl.fedoraproject.org/pub/fedora/linux/releases/",
.versionRegex = "href=\"(\\d+)/\"",
.pageUrl = "https://dl.fedoraproject.org/pub/fedora/linux/releases/%VERSION%/Workstation/x86_64/iso/",
.linkRegex = "href=\"(Fedora-Workstation-Live[\\w.\\-]*x86_64[\\w.\\-]*\\.iso)\"",
.checksumLinkRegex = "href=\"(Fedora-Workstation-[\\w.\\-]*-CHECKSUM)\"",
},
Distro{
.id = "rocky", .name = "Rocky Linux (Minimal)", .family = "Red Hat / Fedora",
.versionUrl = "https://download.rockylinux.org/pub/rocky/",
.versionRegex = "href=\"(\\d+\\.\\d+)/\"",
.checksumUrl = "https://download.rockylinux.org/pub/rocky/%VERSION%/isos/x86_64/CHECKSUM",
.isoPattern = "Rocky-[\\d.]+-x86_64-minimal\\.iso",
.downloadBase = "https://download.rockylinux.org/pub/rocky/%VERSION%/isos/x86_64/",
},
Distro{
.id = "almalinux", .name = "AlmaLinux (Minimal)", .family = "Red Hat / Fedora",
.versionUrl = "https://repo.almalinux.org/almalinux/",
.versionRegex = "href=\"(\\d+\\.\\d+)/\"",
.checksumUrl = "https://repo.almalinux.org/almalinux/%VERSION%/isos/x86_64/CHECKSUM",
.isoPattern = "AlmaLinux-[\\d.]+-x86_64-minimal\\.iso",
.downloadBase = "https://repo.almalinux.org/almalinux/%VERSION%/isos/x86_64/",
},
Distro{
.id = "centos-stream", .name = "CentOS Stream (Boot)", .family = "Red Hat / Fedora",
.versionUrl = "https://mirror.stream.centos.org/",
.versionRegex = "href=\"(\\d+)-stream/\"",
.checksumUrl = "https://mirror.stream.centos.org/%VERSION%-stream/BaseOS/x86_64/iso/CentOS-Stream-%VERSION%-latest-x86_64-boot.iso.SHA256SUM",
.isoPattern = "CentOS-Stream-\\d+-latest-x86_64-boot\\.iso",
.downloadBase = "https://mirror.stream.centos.org/%VERSION%-stream/BaseOS/x86_64/iso/",
},
Distro{
.id = "qubes", .name = "Qubes OS", .family = "Red Hat / Fedora",
.notes = "Zusätzlich GPG-Signatur der DIGESTS-Datei prüfen (qubes-os.org/security).",
.pageUrl = "https://mirrors.edge.kernel.org/qubes/iso/",
.linkRegex = "href=\"(Qubes-R[\\d.]+-x86_64\\.iso)\"",
.checksumUrl = "%ISOURL%.DIGESTS",
.algo = QCryptographicHash::Sha512,
},
// ─────────────── SUSE / Slackware / Mandriva ────────────────────
Distro{
.id = "tumbleweed", .name = "openSUSE Tumbleweed (DVD)", .family = "SUSE / Slackware / Mandriva",
.isoUrl = "https://download.opensuse.org/tumbleweed/iso/openSUSE-Tumbleweed-DVD-x86_64-Current.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "slackware", .name = "Slackware64 (DVD)", .family = "SUSE / Slackware / Mandriva",
.notes = "Projekt bietet nur MD5 an — MD5 ist kryptografisch gebrochen, GPG-Signatur prüfen!",
.versionUrl = "https://mirrors.slackware.com/slackware/slackware-iso/",
.versionRegex = "href=\"slackware64-([\\d.]+)-iso/\"",
.isoUrl = "https://mirrors.slackware.com/slackware/slackware-iso/slackware64-%VERSION%-iso/slackware64-%VERSION%-install-dvd.iso",
.checksumUrl = "%ISOURL%.md5",
.algo = QCryptographicHash::Md5,
},
Distro{
.id = "mageia", .name = "Mageia (Live Plasma)", .family = "SUSE / Slackware / Mandriva",
.versionUrl = "https://mirrors.kernel.org/mageia/iso/",
.versionRegex = "href=\"(\\d+)/\"",
.isoUrl = "https://mirrors.kernel.org/mageia/iso/%VERSION%/Mageia-%VERSION%-Live-Plasma-x86_64/Mageia-%VERSION%-Live-Plasma-x86_64.iso",
.checksumUrl = "%ISOURL%.sha512",
.algo = QCryptographicHash::Sha512,
},
Distro{
.id = "pclinuxos", .name = "PCLinuxOS (KDE)", .family = "SUSE / Slackware / Mandriva",
.notes = "Projekt bietet nur MD5 an — MD5 ist kryptografisch gebrochen.",
.pageUrl = "https://ftp.nluug.nl/os/Linux/distr/pclinuxos/iso/",
.linkRegex = "href=\"(pclinuxos64-kde-[\\d.]+\\.iso)\"",
.checksumLinkRegex = "href=\"(pclinuxos64-kde-[\\d.]+\\.md5sum)\"",
.algo = QCryptographicHash::Md5,
},
// ─────────────────── Unabhängig & Spezial ───────────────────────
Distro{
.id = "void", .name = "Void Linux (Xfce)", .family = "Unabhängig & Spezial",
.checksumUrl = "https://repo-default.voidlinux.org/live/current/sha256sum.txt",
.isoPattern = "void-live-x86_64-\\d+-xfce\\.iso",
.downloadBase = "https://repo-default.voidlinux.org/live/current/",
},
Distro{
.id = "nixos", .name = "NixOS (Graphical)", .family = "Unabhängig & Spezial",
.pageUrl = "https://nixos.org/download/",
.linkRegex = "(https://channels\\.nixos\\.org/nixos-[\\d.]+/latest-nixos-graphical-x86_64-linux\\.iso)",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "gentoo", .name = "Gentoo (Minimal)", .family = "Unabhängig & Spezial",
.pageUrl = "https://distfiles.gentoo.org/releases/amd64/autobuilds/current-install-amd64-minimal/",
.linkRegex = "href=\"(install-amd64-minimal-\\d{8}T\\d{6}Z\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "alpine", .name = "Alpine (Standard)", .family = "Unabhängig & Spezial",
.pageUrl = "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/",
.linkRegex = "href=\"(alpine-standard-[\\d.]+-x86_64\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "deepin", .name = "Deepin", .family = "Unabhängig & Spezial",
.versionUrl = "https://mirrors.kernel.org/deepin-cd/",
.versionRegex = "href=\"(\\d[\\d.]*)/\"",
.checksumUrl = "https://mirrors.kernel.org/deepin-cd/%VERSION%/amd64/SHA256SUMS",
.isoPattern = "deepin-desktop-community-[\\w.\\-]*amd64\\.iso",
.downloadBase = "https://mirrors.kernel.org/deepin-cd/%VERSION%/amd64/",
},
Distro{
.id = "clearlinux", .name = "Clear Linux (Live Desktop)", .family = "Unabhängig & Spezial",
.notes = "Intel hat Clear Linux 2025 eingestellt — Download-Server sind teilweise "
"bereits abgeschaltet, Auflösung kann fehlschlagen.",
.versionUrl = "https://cdn.download.clearlinux.org/latest",
.versionRegex = "(\\d+)",
.isoUrl = "https://cdn.download.clearlinux.org/releases/%VERSION%/clear/clear-%VERSION%-live-desktop.iso",
.checksumUrl = "%ISOURL%-SHA512SUMS",
.algo = QCryptographicHash::Sha512,
},
Distro{
.id = "puppy", .name = "Puppy (BookwormPup64)", .family = "Unabhängig & Spezial",
.versionUrl = "https://distro.ibiblio.org/puppylinux/puppy-bookwormpup/BookwormPup64/",
.versionRegex = "href=\"([\\d.]+)/\"",
.pageUrl = "https://distro.ibiblio.org/puppylinux/puppy-bookwormpup/BookwormPup64/%VERSION%/",
.linkRegex = "href=\"(BookwormPup64_[\\d.]+\\.iso)\"",
.checksumUrl = "%ISOURL%-checksum.txt",
},
Distro{
.id = "tinycore", .name = "Tiny Core (CorePure64)", .family = "Unabhängig & Spezial",
.notes = "Projekt bietet nur HTTP + MD5 an — Download ungesichert, MD5 gebrochen!",
.versionUrl = "http://tinycorelinux.net/downloads.html",
.versionRegex = "(\\d+)\\.x",
.pageUrl = "http://tinycorelinux.net/%VERSION%.x/x86_64/release/",
.linkRegex = "href=\"(CorePure64-[\\d.]+\\.iso)\"",
.checksumUrl = "%ISOURL%.md5.txt",
.algo = QCryptographicHash::Md5,
},
Distro{
.id = "gobolinux", .name = "GoboLinux 017", .family = "Unabhängig & Spezial",
.notes = "Projekt inaktiv (letztes Release 2020), keine Prüfsumme automatisch verfügbar — "
"Link ggf. manuell auf gobolinux.org prüfen.",
.isoUrl = "https://gobolinux.org/release/017/GoboLinux-017-x86_64.iso",
},
Distro{
.id = "kaos", .name = "KaOS", .family = "Unabhängig & Spezial",
.pageUrl = "https://mirror.math.princeton.edu/pub/kaoslinux/",
.linkRegex = "href=\"(KaOS-\\d{4}\\.\\d{2}-x86_64\\.iso)\"",
.checksumLinkRegex = "href=\"(KaOS-\\d{4}\\.\\d{2}-x86_64\\.sha256sum)\"",
},
Distro{
.id = "calculate", .name = "Calculate Linux (Desktop KDE)", .family = "Unabhängig & Spezial",
.versionUrl = "https://mirror.calculate-linux.org/release/",
.versionRegex = "href=\"(\\d{8})/\"",
.checksumUrl = "https://mirror.calculate-linux.org/release/%VERSION%/SHA256SUMS",
.isoPattern = "cld-\\d{8}-x86_64\\.iso",
.downloadBase = "https://mirror.calculate-linux.org/release/%VERSION%/",
},
Distro{
.id = "grml", .name = "Grml (Full)", .family = "Unabhängig & Spezial",
.pageUrl = "https://mirror.de.leaseweb.net/grml/",
.linkRegex = "href=\"(grml-full-\\d{4}\\.\\d{2}-amd64\\.iso)\"",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "systemrescue", .name = "SystemRescue", .family = "Unabhängig & Spezial",
.versionUrl = "https://www.system-rescue.org/Download/",
.versionRegex = "releases/([\\d.]+)/systemrescue-",
.isoUrl = "https://fastly-cdn.system-rescue.org/releases/%VERSION%/systemrescue-%VERSION%-amd64.iso",
.checksumUrl = "%ISOURL%.sha256",
},
Distro{
.id = "proxmox-ve", .name = "Proxmox VE", .family = "Unabhängig & Spezial",
.checksumUrl = "https://enterprise.proxmox.com/iso/SHA256SUMS",
.isoPattern = "proxmox-ve_[\\d.]+-\\d+\\.iso",
.downloadBase = "https://enterprise.proxmox.com/iso/",
},
Distro{
.id = "proxmox-bs", .name = "Proxmox Backup Server", .family = "Unabhängig & Spezial",
.checksumUrl = "https://enterprise.proxmox.com/iso/SHA256SUMS",
.isoPattern = "proxmox-backup-server_[\\d.]+-\\d+\\.iso",
.downloadBase = "https://enterprise.proxmox.com/iso/",
},
Distro{
.id = "truenas-scale", .name = "TrueNAS SCALE", .family = "Unabhängig & Spezial",
.notes = "Release-Train (Fangtooth) ist in der URL kodiert — bei neuem Train anpassen.",
.versionUrl = "https://download.truenas.com/TrueNAS-SCALE-Fangtooth/",
.versionRegex = "href=\"\\.?/?([\\d.]+)/",
.isoUrl = "https://download.truenas.com/TrueNAS-SCALE-Fangtooth/%VERSION%/TrueNAS-SCALE-%VERSION%.iso",
.checksumUrl = "%ISOURL%.sha256",
},
};
return list;
}

104
src/DownloadManager.cpp Normal file
View file

@ -0,0 +1,104 @@
#include "DownloadManager.h"
#include "Resolver.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
DownloadManager::DownloadManager(QNetworkAccessManager* nam, QObject* parent)
: QObject(parent), m_nam(nam) {}
void DownloadManager::start(const QString& url, const QString& targetPath,
QCryptographicHash::Algorithm algo,
const QString& expectedHash)
{
if (m_reply)
return;
m_target = targetPath;
m_algo = algo;
m_expected = expectedHash.toLower();
m_cancelled = false;
m_hasher = std::make_unique<QCryptographicHash>(algo);
m_file.setFileName(targetPath + QStringLiteral(".part"));
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
emit finished(false, false, false,
tr("Datei kann nicht angelegt werden: %1").arg(m_file.errorString()));
return;
}
QNetworkRequest req{QUrl(url)};
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::UserAgentHeader,
QStringLiteral("glorious-iso-dl/1.0"));
m_reply = m_nam->get(req);
connect(m_reply, &QNetworkReply::readyRead, this, [this]() {
const QByteArray chunk = m_reply->readAll();
m_hasher->addData(chunk);
if (m_file.write(chunk) != chunk.size()) {
m_cancelled = true;
m_reply->abort();
}
});
connect(m_reply, &QNetworkReply::downloadProgress,
this, &DownloadManager::progress);
connect(m_reply, &QNetworkReply::finished,
this, &DownloadManager::onReplyFinished);
}
void DownloadManager::cancel()
{
if (m_reply) {
m_cancelled = true;
m_reply->abort();
}
}
void DownloadManager::onReplyFinished()
{
QNetworkReply* reply = m_reply;
m_reply = nullptr;
reply->deleteLater();
m_file.close();
if (m_cancelled) {
m_file.remove();
emit finished(false, false, false, tr("Download abgebrochen."));
return;
}
if (reply->error() != QNetworkReply::NoError) {
m_file.remove();
emit finished(false, false, false,
tr("Download fehlgeschlagen: %1").arg(reply->errorString()));
return;
}
const QString actual = QString::fromLatin1(m_hasher->result().toHex());
const QString algoStr = Resolver::algoName(m_algo);
if (m_expected.isEmpty()) {
QFile::remove(m_target);
m_file.rename(m_target);
emit finished(true, false, false,
tr("Download abgeschlossen — ACHTUNG: keine Prüfsumme verfügbar!\n"
"%1 der Datei: %2\nBitte manuell verifizieren.")
.arg(algoStr, actual));
return;
}
if (actual == m_expected) {
QFile::remove(m_target);
m_file.rename(m_target);
emit finished(true, true, true,
tr("Download abgeschlossen, %1-Prüfsumme OK:\n%2")
.arg(algoStr, actual));
} else {
m_file.remove(); // manipulierte/defekte Datei nicht behalten
emit finished(false, true, false,
tr("PRÜFSUMMENFEHLER! Datei wurde verworfen.\n"
"Erwartet (%1): %2\nErhalten: %3")
.arg(algoStr, m_expected, actual));
}
}

38
src/DownloadManager.h Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include <QObject>
#include <QFile>
#include <QCryptographicHash>
#include <memory>
class QNetworkAccessManager;
class QNetworkReply;
// Lädt eine Datei herunter und berechnet währenddessen den Hash (Streaming).
// Bei Hash-Mismatch wird die Datei verworfen.
class DownloadManager : public QObject {
Q_OBJECT
public:
explicit DownloadManager(QNetworkAccessManager* nam, QObject* parent = nullptr);
void start(const QString& url, const QString& targetPath,
QCryptographicHash::Algorithm algo, const QString& expectedHash);
void cancel();
bool isActive() const { return m_reply != nullptr; }
signals:
void progress(qint64 received, qint64 total);
void finished(bool ok, bool hashChecked, bool hashOk, const QString& message);
private:
void onReplyFinished();
QNetworkAccessManager* m_nam;
QNetworkReply* m_reply = nullptr;
QFile m_file;
std::unique_ptr<QCryptographicHash> m_hasher;
QCryptographicHash::Algorithm m_algo = QCryptographicHash::Sha256;
QString m_expected;
QString m_target;
bool m_cancelled = false;
};

304
src/MainWindow.cpp Normal file
View file

@ -0,0 +1,304 @@
#include "MainWindow.h"
#include "DownloadManager.h"
#include <QApplication>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QPlainTextEdit>
#include <QProgressBar>
#include <QPushButton>
#include <QSplitter>
#include <QStandardPaths>
#include <QStatusBar>
#include <QTime>
#include <QTreeWidget>
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
m_nam = new QNetworkAccessManager(this);
m_resolver = new Resolver(m_nam, this);
m_downloader = new DownloadManager(m_nam, this);
connect(m_resolver, &Resolver::progress, this, &MainWindow::log);
connect(m_resolver, &Resolver::resolved, this, &MainWindow::onResolved);
connect(m_resolver, &Resolver::failed, this, &MainWindow::onResolveFailed);
connect(m_downloader, &DownloadManager::progress, this, &MainWindow::onDownloadProgress);
connect(m_downloader, &DownloadManager::finished, this, &MainWindow::onDownloadFinished);
buildUi();
setWindowTitle(tr("Glorious ISO Downloader"));
resize(1100, 700);
}
void MainWindow::buildUi()
{
auto* splitter = new QSplitter(this);
// Linke Seite: Distro-Baum, gruppiert nach Familie
m_tree = new QTreeWidget;
m_tree->setHeaderLabel(tr("Distribution"));
QString lastFamily;
QTreeWidgetItem* familyItem = nullptr;
for (const Distro& d : DistroRegistry::all()) {
if (d.family != lastFamily) {
familyItem = new QTreeWidgetItem(m_tree, {d.family});
familyItem->setFlags(Qt::ItemIsEnabled);
QFont f = familyItem->font(0);
f.setBold(true);
familyItem->setFont(0, f);
familyItem->setExpanded(true);
lastFamily = d.family;
}
auto* item = new QTreeWidgetItem(familyItem, {d.name});
item->setData(0, Qt::UserRole, d.id);
}
connect(m_tree, &QTreeWidget::currentItemChanged, this, &MainWindow::onSelectionChanged);
splitter->addWidget(m_tree);
// Rechte Seite
auto* right = new QWidget;
auto* layout = new QVBoxLayout(right);
m_titleLabel = new QLabel(tr("Bitte eine Distribution auswählen."));
QFont tf = m_titleLabel->font();
tf.setPointSizeF(tf.pointSizeF() * 1.4);
tf.setBold(true);
m_titleLabel->setFont(tf);
layout->addWidget(m_titleLabel);
m_notesLabel = new QLabel;
m_notesLabel->setWordWrap(true);
m_notesLabel->setStyleSheet(QStringLiteral("color: #c08000;"));
m_notesLabel->hide();
layout->addWidget(m_notesLabel);
m_resolveBtn = new QPushButton(tr("Aktuellen Download-Link ermitteln"));
m_resolveBtn->setEnabled(false);
connect(m_resolveBtn, &QPushButton::clicked, this, &MainWindow::onResolveClicked);
layout->addWidget(m_resolveBtn);
auto makeInfoLabel = []() {
auto* l = new QLabel(QStringLiteral(""));
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
l->setWordWrap(true);
return l;
};
auto* infoLayout = new QVBoxLayout;
infoLayout->addWidget(new QLabel(tr("<b>Version:</b>")));
infoLayout->addWidget(m_versionLabel = makeInfoLabel());
infoLayout->addWidget(new QLabel(tr("<b>URL:</b>")));
infoLayout->addWidget(m_urlLabel = makeInfoLabel());
infoLayout->addWidget(new QLabel(tr("<b>Erwartete Prüfsumme:</b>")));
infoLayout->addWidget(m_hashLabel = makeInfoLabel());
layout->addLayout(infoLayout);
auto* dirLayout = new QHBoxLayout;
dirLayout->addWidget(new QLabel(tr("Zielordner:")));
m_dirEdit = new QLineEdit(
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
dirLayout->addWidget(m_dirEdit, 1);
auto* browseBtn = new QPushButton(tr(""));
connect(browseBtn, &QPushButton::clicked, this, [this]() {
const QString dir = QFileDialog::getExistingDirectory(
this, tr("Zielordner wählen"), m_dirEdit->text());
if (!dir.isEmpty())
m_dirEdit->setText(dir);
});
dirLayout->addWidget(browseBtn);
layout->addLayout(dirLayout);
auto* btnLayout = new QHBoxLayout;
m_downloadBtn = new QPushButton(tr("Herunterladen && verifizieren"));
m_downloadBtn->setEnabled(false);
connect(m_downloadBtn, &QPushButton::clicked, this, &MainWindow::onDownloadClicked);
btnLayout->addWidget(m_downloadBtn);
m_cancelBtn = new QPushButton(tr("Abbrechen"));
m_cancelBtn->setEnabled(false);
connect(m_cancelBtn, &QPushButton::clicked, m_downloader, &DownloadManager::cancel);
btnLayout->addWidget(m_cancelBtn);
layout->addLayout(btnLayout);
m_progress = new QProgressBar;
m_progress->setRange(0, 100);
m_progress->setValue(0);
layout->addWidget(m_progress);
m_log = new QPlainTextEdit;
m_log->setReadOnly(true);
layout->addWidget(m_log, 1);
splitter->addWidget(right);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
splitter->setSizes({320, 780});
setCentralWidget(splitter);
statusBar()->showMessage(tr("Bereit"));
}
void MainWindow::log(const QString& msg)
{
m_log->appendPlainText(QStringLiteral("[%1] %2")
.arg(QTime::currentTime().toString(QStringLiteral("HH:mm:ss")), msg));
}
const Distro* MainWindow::currentDistro() const
{
QTreeWidgetItem* item = m_tree->currentItem();
if (!item)
return nullptr;
const QString id = item->data(0, Qt::UserRole).toString();
if (id.isEmpty())
return nullptr;
for (const Distro& d : DistroRegistry::all())
if (d.id == id)
return &d;
return nullptr;
}
void MainWindow::resetResolvedState()
{
m_hasResolved = false;
m_resolved = {};
m_versionLabel->setText(QStringLiteral(""));
m_urlLabel->setText(QStringLiteral(""));
m_hashLabel->setText(QStringLiteral(""));
m_hashLabel->setStyleSheet({});
m_downloadBtn->setEnabled(false);
m_progress->setValue(0);
}
void MainWindow::onSelectionChanged()
{
m_resolver->abort();
resetResolvedState();
const Distro* d = currentDistro();
m_resolveBtn->setEnabled(d != nullptr && !m_downloader->isActive());
if (!d) {
m_titleLabel->setText(tr("Bitte eine Distribution auswählen."));
m_notesLabel->hide();
return;
}
m_titleLabel->setText(d->name);
m_notesLabel->setVisible(!d->notes.isEmpty());
m_notesLabel->setText(d->notes.isEmpty() ? QString()
: tr("Hinweis: %1").arg(d->notes));
}
void MainWindow::onResolveClicked()
{
const Distro* d = currentDistro();
if (!d)
return;
resetResolvedState();
m_resolveBtn->setEnabled(false);
log(tr("Löse Download-Link für %1 auf …").arg(d->name));
m_resolver->resolve(*d);
}
void MainWindow::onResolved(const ResolvedIso& iso)
{
m_resolved = iso;
m_hasResolved = true;
m_resolveBtn->setEnabled(true);
m_versionLabel->setText(iso.version.isEmpty() ? tr("aktuell (rolling/latest)") : iso.version);
m_urlLabel->setText(iso.url);
if (iso.expectedHash.isEmpty()) {
m_hashLabel->setText(tr("KEINE Prüfsumme verfügbar — Verifikation nicht möglich!"));
m_hashLabel->setStyleSheet(QStringLiteral("color: #cc2222; font-weight: bold;"));
} else {
m_hashLabel->setText(QStringLiteral("%1: %2")
.arg(Resolver::algoName(iso.algo), iso.expectedHash));
m_hashLabel->setStyleSheet({});
}
m_downloadBtn->setEnabled(!m_downloader->isActive());
log(tr("Aufgelöst: %1").arg(iso.url));
statusBar()->showMessage(tr("Link aufgelöst: %1").arg(iso.fileName));
}
void MainWindow::onResolveFailed(const QString& error)
{
m_resolveBtn->setEnabled(currentDistro() != nullptr);
log(tr("Fehler: %1").arg(error));
statusBar()->showMessage(tr("Auflösung fehlgeschlagen"));
}
void MainWindow::onDownloadClicked()
{
if (!m_hasResolved || m_downloader->isActive())
return;
if (m_resolved.url.startsWith(QStringLiteral("http://"))) {
if (QMessageBox::warning(this, tr("Unverschlüsselter Download"),
tr("Dieser Download nutzt unverschlüsseltes HTTP und kann "
"unterwegs manipuliert werden. Trotzdem fortfahren?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
return;
}
if (m_resolved.expectedHash.isEmpty()) {
if (QMessageBox::warning(this, tr("Keine Prüfsumme"),
tr("Für dieses ISO ist keine Prüfsumme verfügbar — die Datei "
"kann nicht automatisch verifiziert werden. Fortfahren?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
return;
}
const QString target = m_dirEdit->text() + QLatin1Char('/') + m_resolved.fileName;
if (QFile::exists(target)) {
if (QMessageBox::question(this, tr("Datei existiert"),
tr("%1 existiert bereits. Überschreiben?").arg(target))
!= QMessageBox::Yes)
return;
}
m_downloadBtn->setEnabled(false);
m_resolveBtn->setEnabled(false);
m_cancelBtn->setEnabled(true);
log(tr("Starte Download nach %1").arg(target));
m_downloader->start(m_resolved.url, target, m_resolved.algo, m_resolved.expectedHash);
}
void MainWindow::onDownloadProgress(qint64 received, qint64 total)
{
if (total > 0) {
m_progress->setRange(0, 100);
m_progress->setValue(static_cast<int>(received * 100 / total));
statusBar()->showMessage(tr("%1 / %2 MiB")
.arg(received / 1048576.0, 0, 'f', 1)
.arg(total / 1048576.0, 0, 'f', 1));
} else {
m_progress->setRange(0, 0); // unbestimmt
statusBar()->showMessage(tr("%1 MiB").arg(received / 1048576.0, 0, 'f', 1));
}
}
void MainWindow::onDownloadFinished(bool ok, bool hashChecked, bool hashOk,
const QString& message)
{
m_cancelBtn->setEnabled(false);
m_resolveBtn->setEnabled(currentDistro() != nullptr);
m_downloadBtn->setEnabled(m_hasResolved);
m_progress->setRange(0, 100);
m_progress->setValue(ok ? 100 : 0);
log(message);
if (ok && hashChecked && hashOk) {
statusBar()->showMessage(tr("Fertig — Prüfsumme OK"));
QMessageBox::information(this, tr("Erfolg"), message);
} else if (ok) {
statusBar()->showMessage(tr("Fertig — NICHT verifiziert"));
QMessageBox::warning(this, tr("Nicht verifiziert"), message);
} else if (hashChecked && !hashOk) {
statusBar()->showMessage(tr("PRÜFSUMMENFEHLER"));
QMessageBox::critical(this, tr("Prüfsummenfehler"), message);
} else {
statusBar()->showMessage(tr("Download fehlgeschlagen"));
}
}

56
src/MainWindow.h Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include <QMainWindow>
#include "Distro.h"
#include "Resolver.h"
class QTreeWidget;
class QTreeWidgetItem;
class QLabel;
class QLineEdit;
class QPushButton;
class QProgressBar;
class QPlainTextEdit;
class QNetworkAccessManager;
class DownloadManager;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void onSelectionChanged();
void onResolveClicked();
void onResolved(const ResolvedIso& iso);
void onResolveFailed(const QString& error);
void onDownloadClicked();
void onDownloadProgress(qint64 received, qint64 total);
void onDownloadFinished(bool ok, bool hashChecked, bool hashOk, const QString& message);
private:
void buildUi();
void log(const QString& msg);
const Distro* currentDistro() const;
void resetResolvedState();
QNetworkAccessManager* m_nam = nullptr;
Resolver* m_resolver = nullptr;
DownloadManager* m_downloader = nullptr;
QTreeWidget* m_tree = nullptr;
QLabel* m_titleLabel = nullptr;
QLabel* m_notesLabel = nullptr;
QLabel* m_versionLabel = nullptr;
QLabel* m_urlLabel = nullptr;
QLabel* m_hashLabel = nullptr;
QLineEdit* m_dirEdit = nullptr;
QPushButton* m_resolveBtn = nullptr;
QPushButton* m_downloadBtn = nullptr;
QPushButton* m_cancelBtn = nullptr;
QProgressBar* m_progress = nullptr;
QPlainTextEdit* m_log = nullptr;
ResolvedIso m_resolved;
bool m_hasResolved = false;
};

307
src/Resolver.cpp Normal file
View file

@ -0,0 +1,307 @@
#include "Resolver.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QUrl>
Resolver::Resolver(QNetworkAccessManager* nam, QObject* parent)
: QObject(parent), m_nam(nam) {}
QString Resolver::algoName(QCryptographicHash::Algorithm a)
{
switch (a) {
case QCryptographicHash::Md5: return QStringLiteral("MD5");
case QCryptographicHash::Sha1: return QStringLiteral("SHA-1");
case QCryptographicHash::Sha256: return QStringLiteral("SHA-256");
case QCryptographicHash::Sha512: return QStringLiteral("SHA-512");
default: return QStringLiteral("Hash");
}
}
void Resolver::resolve(const Distro& d)
{
++m_gen;
m_d = d;
m_versionCandidates.clear();
m_version.clear();
m_isoUrl.clear();
m_fileName.clear();
m_hash.clear();
stepVersion();
}
void Resolver::abort()
{
++m_gen;
}
QString Resolver::subst(QString s) const
{
s.replace(QStringLiteral("%VERSION%"), m_version);
s.replace(QStringLiteral("%ISOURL%"), m_isoUrl);
s.replace(QStringLiteral("%FILENAME%"), m_fileName);
return s;
}
void Resolver::fetch(const QString& url,
std::function<void(const QString&, const QString&)> cb)
{
QNetworkRequest req{QUrl(url)};
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::UserAgentHeader,
QStringLiteral("glorious-iso-dl/1.0"));
QNetworkReply* reply = m_nam->get(req);
const int gen = m_gen;
connect(reply, &QNetworkReply::finished, this, [this, reply, cb, url, gen]() {
reply->deleteLater();
if (gen != m_gen)
return; // veralteter Aufruf
if (reply->error() != QNetworkReply::NoError)
cb({}, tr("%1: %2").arg(url, reply->errorString()));
else
cb(QString::fromUtf8(reply->readAll()), {});
});
}
QStringList Resolver::allMatches(const QString& body, const QString& pattern, int group)
{
QStringList out;
QRegularExpression re(pattern);
auto it = re.globalMatch(body);
while (it.hasNext()) {
const QString m = it.next().captured(group);
if (!m.isEmpty() && !out.contains(m))
out << m;
}
return out;
}
int Resolver::versionCompare(const QString& a, const QString& b)
{
// Vergleicht versionsbewusst: Ziffernfolgen numerisch, Rest lexikalisch.
static const QRegularExpression tok(QStringLiteral("(\\d+|\\D+)"));
auto tokens = [](const QString& s) {
QStringList t;
auto it = tok.globalMatch(s);
while (it.hasNext())
t << it.next().captured(1);
return t;
};
const QStringList ta = tokens(a), tb = tokens(b);
const int n = qMin(ta.size(), tb.size());
for (int i = 0; i < n; ++i) {
bool na = false, nb = false;
const qlonglong va = ta[i].toLongLong(&na);
const qlonglong vb = tb[i].toLongLong(&nb);
if (na && nb) {
if (va != vb)
return va < vb ? -1 : 1;
} else {
const int c = QString::compare(ta[i], tb[i]);
if (c != 0)
return c;
}
}
return ta.size() - tb.size();
}
QString Resolver::pickHighest(QStringList list)
{
std::sort(list.begin(), list.end(),
[](const QString& a, const QString& b) { return versionCompare(a, b) < 0; });
return list.isEmpty() ? QString() : list.last();
}
QString Resolver::parseChecksumFile(const QString& body, const QString& fileName,
QCryptographicHash::Algorithm algo)
{
const int len = QCryptographicHash::hashLength(algo) * 2;
const QString base = QUrl(fileName).fileName();
const QString algoKey = algoName(algo).remove(QLatin1Char('-')); // "SHA256" usw.
const QRegularExpression lineStd(
QStringLiteral("^\\s*([0-9a-fA-F]{%1})[\\s*]+\\*?(.+?)\\s*$").arg(len));
const QRegularExpression lineBsd(
QStringLiteral("^\\s*([A-Za-z0-9-]+)\\s*\\((.+)\\)\\s*=\\s*([0-9a-fA-F]{%1})\\s*$").arg(len));
QString sectionLabel; // letzter Kommentar (z. B. "# SHA512 HASH" bei Gentoo)
QStringList bareHashes;
QStringList lineHashes; // alle Hashes aus Zeilen mit (fremdem) Dateinamen
for (const QString& raw : body.split(QLatin1Char('\n'))) {
const QString line = raw.trimmed();
if (line.startsWith(QLatin1Char('#'))) {
sectionLabel = line.toUpper();
continue;
}
// Abschnitt gehört erkennbar zu einem anderen Algorithmus -> überspringen
if (!sectionLabel.isEmpty() && sectionLabel.contains(QStringLiteral("HASH"))
&& !sectionLabel.contains(algoKey))
continue;
if (auto m = lineBsd.match(line); m.hasMatch()) {
// BSD-Format: "SHA256 (datei.iso) = hash"
const QString label = m.captured(1).toUpper().remove(QLatin1Char('-'));
const QString name = QUrl(m.captured(2).trimmed()).fileName();
if (label == algoKey && (name == base || base.isEmpty()))
return m.captured(3).toLower();
continue;
}
if (auto m = lineStd.match(line); m.hasMatch()) {
const QString name = QUrl(m.captured(2).trimmed()).fileName();
if (name == base || (base.isEmpty() && !name.isEmpty()))
return m.captured(1).toLower();
lineHashes << m.captured(1).toLower();
continue;
}
// Zeile besteht nur aus einem Hash (typisch für *.sha256-Einzeldateien)
static const QRegularExpression bareRe(QStringLiteral("^[0-9a-fA-F]+$"));
if (line.size() == len && bareRe.match(line).hasMatch())
bareHashes << line.toLower();
}
// Fallback 1: genau ein nackter Hash in der Datei
if (bareHashes.size() == 1)
return bareHashes.first();
// Fallback 2: genau eine Hash-Zeile, deren Dateiname nicht passt — typisch
// für "latest.iso"-Aliasse, bei denen die Prüfsummendatei den echten Namen führt
if (bareHashes.isEmpty() && lineHashes.size() == 1)
return lineHashes.first();
return {};
}
void Resolver::stepVersion()
{
if (m_d.versionUrl.isEmpty()) {
stepPage();
return;
}
emit progress(tr("Ermittle neueste Version …"));
fetch(subst(m_d.versionUrl), [this](const QString& body, const QString& err) {
if (!err.isEmpty()) { emit failed(err); return; }
QStringList found = allMatches(body, m_d.versionRegex, 1);
if (found.isEmpty()) {
emit failed(tr("Keine Version gefunden auf %1").arg(m_d.versionUrl));
return;
}
std::sort(found.begin(), found.end(),
[](const QString& a, const QString& b) { return versionCompare(a, b) > 0; });
// Höchste Version zuerst; einige Reserven für unveröffentlichte Verzeichnisse
m_versionCandidates = found.mid(0, 6);
m_version = m_versionCandidates.takeFirst();
emit progress(tr("Neueste Version: %1").arg(m_version));
stepPage();
});
}
bool Resolver::retryNextVersion()
{
if (m_versionCandidates.isEmpty())
return false;
m_version = m_versionCandidates.takeFirst();
m_isoUrl.clear();
m_fileName.clear();
m_hash.clear();
emit progress(tr("Noch nicht veröffentlicht — versuche Version %1 …").arg(m_version));
stepPage();
return true;
}
void Resolver::stepPage()
{
if (m_d.pageUrl.isEmpty()) {
stepChecksum();
return;
}
emit progress(tr("Suche ISO-Link …"));
const QString pageUrl = subst(m_d.pageUrl);
fetch(pageUrl, [this, pageUrl](const QString& body, const QString& err) {
if (!err.isEmpty()) {
if (retryNextVersion())
return;
emit failed(err);
return;
}
if (!m_d.linkRegex.isEmpty()) {
const QStringList links = allMatches(body, subst(m_d.linkRegex), 1);
if (links.isEmpty()) {
if (retryNextVersion())
return;
emit failed(tr("Kein ISO-Link gefunden auf %1").arg(pageUrl));
return;
}
const QString link = pickHighest(links);
m_isoUrl = QUrl(pageUrl).resolved(QUrl(link)).toString();
m_fileName = QUrl(m_isoUrl).fileName();
}
if (!m_d.checksumLinkRegex.isEmpty()) {
const QStringList cs = allMatches(body, subst(m_d.checksumLinkRegex), 1);
if (!cs.isEmpty())
m_d.checksumUrl = QUrl(pageUrl).resolved(QUrl(pickHighest(cs))).toString();
}
if (!m_d.hashRegex.isEmpty()) {
const QStringList h = allMatches(body, subst(m_d.hashRegex), 1);
if (!h.isEmpty())
m_hash = h.first().toLower();
}
stepChecksum();
});
}
void Resolver::stepChecksum()
{
if (m_isoUrl.isEmpty() && !m_d.isoUrl.isEmpty()) {
m_isoUrl = subst(m_d.isoUrl);
m_fileName = QUrl(m_isoUrl).fileName();
}
if (!m_hash.isEmpty()) { finish(); return; }
if (m_d.checksumUrl.isEmpty()) { finish(); return; }
emit progress(tr("Lade Prüfsummen …"));
fetch(subst(m_d.checksumUrl), [this](const QString& body, const QString& err) {
if (!err.isEmpty()) {
if (retryNextVersion())
return;
emit failed(tr("Prüfsummendatei nicht erreichbar: %1").arg(err));
return;
}
if (!m_d.isoPattern.isEmpty()) {
// Listenmodus: Dateiname aus der Prüfsummendatei ermitteln
const QStringList files = allMatches(
body, QStringLiteral("(%1)").arg(subst(m_d.isoPattern)), 1);
if (files.isEmpty()) {
if (retryNextVersion())
return;
emit failed(tr("Kein passendes ISO in der Prüfsummendatei gefunden."));
return;
}
m_fileName = pickHighest(files);
m_isoUrl = subst(m_d.downloadBase) + m_fileName;
}
m_hash = parseChecksumFile(body, m_fileName, m_d.algo);
if (m_hash.isEmpty()) {
emit failed(tr("Kein %1-Hash für %2 in der Prüfsummendatei gefunden.")
.arg(algoName(m_d.algo), m_fileName));
return;
}
finish();
});
}
void Resolver::finish()
{
if (m_isoUrl.isEmpty()) {
emit failed(tr("ISO-URL konnte nicht ermittelt werden."));
return;
}
ResolvedIso r;
r.url = m_isoUrl;
r.fileName = m_fileName.isEmpty() ? QUrl(m_isoUrl).fileName() : m_fileName;
r.version = m_version;
r.expectedHash = m_hash;
r.algo = m_d.algo;
emit resolved(r);
}

58
src/Resolver.h Normal file
View file

@ -0,0 +1,58 @@
#pragma once
#include <QObject>
#include <functional>
#include "Distro.h"
class QNetworkAccessManager;
struct ResolvedIso {
QString url;
QString fileName;
QString version;
QString expectedHash; // hex, klein; leer = keine Prüfsumme verfügbar
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256;
};
class Resolver : public QObject {
Q_OBJECT
public:
explicit Resolver(QNetworkAccessManager* nam, QObject* parent = nullptr);
void resolve(const Distro& d);
void abort();
static QString algoName(QCryptographicHash::Algorithm a);
signals:
void progress(const QString& msg);
void resolved(const ResolvedIso& iso);
void failed(const QString& error);
private:
void stepVersion();
void stepPage();
void stepChecksum();
void finish();
void fetch(const QString& url,
std::function<void(const QString& body, const QString& error)> cb);
QString subst(QString s) const;
static QStringList allMatches(const QString& body, const QString& pattern, int group);
static QString pickHighest(QStringList list);
static int versionCompare(const QString& a, const QString& b);
static QString parseChecksumFile(const QString& body, const QString& fileName,
QCryptographicHash::Algorithm algo);
bool retryNextVersion(); // Fallback: nächstniedrigere Version probieren
QNetworkAccessManager* m_nam;
Distro m_d;
int m_gen = 0; // entwertet Callbacks älterer resolve()-Aufrufe
QStringList m_versionCandidates; // absteigend sortiert
QString m_version;
QString m_isoUrl;
QString m_fileName;
QString m_hash;
};

13
src/main.cpp Normal file
View file

@ -0,0 +1,13 @@
#include <QApplication>
#include "MainWindow.h"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QApplication::setApplicationName(QStringLiteral("glorious-iso-dl"));
QApplication::setApplicationVersion(QStringLiteral("1.0"));
MainWindow win;
win.show();
return app.exec();
}