ISO Downloader with native GUI and checksum verification
This commit is contained in:
commit
3038fcf94c
12 changed files with 1470 additions and 0 deletions
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal 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
22
CMakeLists.txt
Normal 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
91
README.md
Normal 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
47
src/Distro.h
Normal 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
383
src/DistroRegistry.cpp
Normal 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
104
src/DownloadManager.cpp
Normal 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
38
src/DownloadManager.h
Normal 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
304
src/MainWindow.cpp
Normal 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
56
src/MainWindow.h
Normal 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
307
src/Resolver.cpp
Normal 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
58
src/Resolver.h
Normal 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
13
src/main.cpp
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue