diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..6c43dcc
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+target/
+!target/release/mirror-package
+!target/x86_64-unknown-linux-gnu/release/mirror-package
+.git/
+.github/
+.gitea/
+tests/
diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml
index a51f514..c6434ec 100644
--- a/.gitea/workflows/main.yaml
+++ b/.gitea/workflows/main.yaml
@@ -181,3 +181,34 @@ jobs:
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
echo "Asset ${filename} erfolgreich hochgeladen."
done
+
+ - name: Build and Publish Docker Container Image (Stable)
+ env:
+ GITEA_URL: ${{ gitea.server_url || github.server_url }}
+ REPO: ${{ gitea.repository || github.repository }}
+ REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }}
+ REPO_NAME: ${{ gitea.repository_name || github.event.repository.name }}
+ TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
+ run: |
+ VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
+ REGISTRY_HOST=$(echo "${GITEA_URL}" | sed -E 's|^https?://||; s|/.*$||')
+ IMAGE_NAME=$(echo "${REGISTRY_HOST}/${REPO_OWNER}/${REPO_NAME}" | tr '[:upper:]' '[:lower:]')
+
+ echo "Authentifiziere an der Container Registry ${REGISTRY_HOST}..."
+ echo "${TOKEN}" | docker login "${REGISTRY_HOST}" -u "${REPO_OWNER}" --password-stdin
+
+ echo "Baue Docker-Container Image (Stable)..."
+ docker build \
+ --build-arg TARGET_BIN=target/x86_64-unknown-linux-gnu/release/mirror-package \
+ -t "${IMAGE_NAME}:latest" \
+ -t "${IMAGE_NAME}:${VERSION}" \
+ -t "${IMAGE_NAME}:v${VERSION}" \
+ -t "${IMAGE_NAME}:${VERSION}.${BUILD_NUMBER}" \
+ .
+
+ echo "Veröffentliche Docker-Images nach ${IMAGE_NAME}..."
+ docker push "${IMAGE_NAME}:latest"
+ docker push "${IMAGE_NAME}:${VERSION}"
+ docker push "${IMAGE_NAME}:v${VERSION}"
+ docker push "${IMAGE_NAME}:${VERSION}.${BUILD_NUMBER}"
+ echo "Docker-Images (Stable & latest) erfolgreich veröffentlicht."
diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml
index eb05f93..47047aa 100644
--- a/.gitea/workflows/testing.yaml
+++ b/.gitea/workflows/testing.yaml
@@ -181,3 +181,36 @@ jobs:
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
echo "Asset ${filename} erfolgreich hochgeladen."
done
+
+ - name: Build and Publish Docker Container Image (Testing)
+ env:
+ GITEA_URL: ${{ gitea.server_url || github.server_url }}
+ REPO: ${{ gitea.repository || github.repository }}
+ REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }}
+ REPO_NAME: ${{ gitea.repository_name || github.event.repository.name }}
+ TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
+ run: |
+ VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
+ REGISTRY_HOST=$(echo "${GITEA_URL}" | sed -E 's|^https?://||; s|/.*$||')
+ IMAGE_NAME=$(echo "${REGISTRY_HOST}/${REPO_OWNER}/${REPO_NAME}" | tr '[:upper:]' '[:lower:]')
+
+ echo "Authentifiziere an der Container Registry ${REGISTRY_HOST}..."
+ echo "${TOKEN}" | docker login "${REGISTRY_HOST}" -u "${REPO_OWNER}" --password-stdin
+
+ echo "Baue Docker-Container Image (Testing / Preview)..."
+ docker build \
+ --build-arg TARGET_BIN=target/x86_64-unknown-linux-gnu/release/mirror-package \
+ -t "${IMAGE_NAME}:testing" \
+ -t "${IMAGE_NAME}:${VERSION}-preview" \
+ -t "${IMAGE_NAME}:${VERSION}-testing" \
+ -t "${IMAGE_NAME}:${VERSION}-preview.${BUILD_NUMBER}" \
+ -t "${IMAGE_NAME}:testing-${BUILD_NUMBER}" \
+ .
+
+ echo "Veröffentliche Docker-Images nach ${IMAGE_NAME}..."
+ docker push "${IMAGE_NAME}:testing"
+ docker push "${IMAGE_NAME}:${VERSION}-preview"
+ docker push "${IMAGE_NAME}:${VERSION}-testing"
+ docker push "${IMAGE_NAME}:${VERSION}-preview.${BUILD_NUMBER}"
+ docker push "${IMAGE_NAME}:testing-${BUILD_NUMBER}"
+ echo "Docker-Images (Testing) erfolgreich veröffentlicht."
diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
new file mode 100644
index 0000000..0ae7be4
--- /dev/null
+++ b/.idea/jsonSchemas.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/rust-template.iml b/.idea/rust-template.iml
index cf84ae4..bbe0a70 100644
--- a/.idea/rust-template.iml
+++ b/.idea/rust-template.iml
@@ -3,6 +3,7 @@
+
diff --git a/AGENTS.md b/AGENTS.md
index a90b81a..275cfca 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,21 +1,26 @@
# AGENTS.md
-Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Coding-Agenten (sowie Entwickler), die an diesem Repository oder daraus abgeleiteten Projekten arbeiten.
+Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Coding-Agenten (sowie Entwickler), die an `mirror-package` arbeiten.
---
## 1. Projektübersicht & Philosophie
-Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
-- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`).
-- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`).
-- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
+`mirror-package` ist eine Rust-basierte CLI-Anwendung zur automatisierten Synchronisation und Spiegelung von Linux-Paketen aus GitHub-Releases in selbstgehostete Gitea/Forgejo-Paket-Registries.
+
+### Kernfunktionen & Routing
+- **Unterstützte Paketformate**: Debian (`.deb`), Fedora/RHEL (`.rpm`), Arch Linux (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`).
+- **Distributions-Routing**:
+ - Stabile Releases: `.deb` wird in `pool/stable/main` **und** `pool/testing/main` geladen; `.rpm` nach `rpm/stable`; Arch nach `arch/stable`.
+ - Pre-Releases: `.deb` wird ausschließlich nach `pool/testing/main` geladen; `.rpm` nach `rpm/testing`; Arch nach `arch/testing`.
+- **Konfigurationsverwaltung**: Typensichere Speicherung über `config-ctdra`.
+- **Authentifizierung**: Tokenloser Zugriff auf öffentliche GitHub-Releases (optionaler Token konfigurierbar); Gitea-Token/URL/Owner via CLI, Env-Vars oder Config.
### Standards & Tech-Stack
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
- **Rust Toolchain**: Stable.
- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`).
-- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert).
+- **Lizenz**: GPL-3.0-or-later.
---
@@ -32,10 +37,23 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C
│ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer
│ └── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
├── src/
-│ └── main.rs # Einstiegspunkt der Anwendung
+│ ├── main.rs # Einstiegspunkt der Anwendung & Befehlsausführung
+│ ├── lib.rs # Bibliotheks-Wurzel & Modulexporte
+│ ├── cli.rs # Clap CLI-Parser & Subcommands
+│ ├── config.rs # Konfigurationsmodelle & config-ctdra Anbindung
+│ ├── gitea.rs # Gitea Package API Client & Routing-Logik
+│ ├── github.rs # GitHub API Client & Paket-Klassifizierung
+│ └── pipeline.rs # Synchronisations-Pipeline & Tempfile-Management
+├── tests/
+│ ├── config_tests.rs # Tests für Konfigurationsverwaltung & Pfade
+│ ├── gitea_tests.rs # Tests für Gitea Routing & Endpunkte
+│ └── github_tests.rs # Tests für Paket-Klassifizierung & Repo-Parsing
+├── .dockerignore # Ausschlussregeln für Container-Builds
+├── Dockerfile # Minimales & gehärtetes Runtime-Container-Image
+├── docker-compose.example.yml # Beispielkonfiguration für Docker Compose
├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch
├── LICENSE # Lizenztext
-├── README.md # Benutzerdokumentation & Setup-Checkliste
+├── README.md # Benutzerdokumentation
└── AGENTS.md # Dieses Agenten-Handbuch
```
@@ -45,9 +63,9 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C
### 3.1 Code-Stil & Best Practices
- **Idiomatisches Rust**: Nutze moderne Sprachfeatures der Rust Edition 2024.
-- **Fehlerbehandlung**: Verwende aussagekräftige Fehlertypen (z. B. mit `thiserror` oder `anyhow` für CLIs). Vermeide unnötiges `unwrap()` oder `panic!()` im Produktivcode.
-- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen. Behalte die bestehende Sprachkonvention bei.
-- **Template-TODOs**: Wenn neue Vorlagen-Features oder Platzhalter ergänzt werden, markiere anpassungsbedürftige Stellen eindeutig mit `// TODO:` (Rust), `# TODO:` (TOML/Python/YAML).
+- **Fehlerbehandlung**: Verwende aussagekräftige Fehlertypen (`anyhow` für CLI und Pipeline-Fehler). Vermeide `unwrap()` oder `panic!()` im Produktivcode.
+- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen.
+- **Logging**: Nutze `logger-ctdra` für strukturierte Konsolen- und Datei-Logs.
### 3.2 Paketierungs-Metadaten in `Cargo.toml`
Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die drei Metadaten-Blöcke in `Cargo.toml` synchron gehalten werden:
@@ -59,15 +77,23 @@ Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die
- `pkgrel`, `arch`, `depends`, `optdepends`.
### 3.3 Skripte in `scripts/`
-- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` (via `cargo metadata` oder Dateiparsing) oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden.
-- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek: `json`, `subprocess`, `urllib`, `argparse`, `os`, `re`, `tempfile`, `tarfile` etc.).
+- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden.
+- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek).
+
+### 3.4 Container-Sicherheit & Persistenz
+- **Sicherheitsrichtlinien**:
+ - Unprivilegierter Benutzer: Der Container läuft unter `appuser` (UID `10001`, GID `10001`).
+ - Read-Only Rootfs: Kompatibel mit `--read-only` bzw. `read_only: true`.
+ - Berechtigungen: Alle Linux-Capabilities können gedroppt werden (`--cap-drop=ALL`), `no-new-privileges: true`.
+- **Persistente Pfade**:
+ - `/home/appuser/.config/mirror-package`: Speichert Konfiguration (`config.toml`).
+ - `/home/appuser/.local/state/mirror-package`: Speichert Logdateien & Status.
+ - `/tmp`: Temporärer Puffer beim Herunterladen von Paketen (idealerweise via `tmpfs`).
---
## 4. Workflows: Bauen, Testen & Validieren
-Agenten müssen Änderungen vor dem Abschluss validieren.
-
### 4.1 Grundlegende Validierung
```bash
# Syntax- und Typprüfung
@@ -94,7 +120,7 @@ python3 scripts/package-arch.py --arch x86_64 --pkgrel 1
## 5. CI/CD-Pipeline Details
- **Trigger**:
- - `push` auf `main`: Baut Binaries für alle 3 Architekturen, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch und erstellt ein Gitea Release `v`.
- - `push` auf `testing`: Baut Binaries & Pakete für den `testing`-Kanal und erstellt ein Pre-Release `v-preview`.
+ - `push` auf `main`: Baut Binaries für alle 3 Architekturen, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v` und baut/veröffentlicht das Docker-Container-Image mit den Tags `:latest`, `:`, `:v` und `:.`.
+ - `push` auf `testing`: Baut Binaries & Pakete für den `testing`-Kanal, erstellt ein Pre-Release `v-preview` und baut/veröffentlicht das Docker-Container-Image ausschließlich mit eindeutigen Testing-Tags (`:testing`, `:-preview`, `:-testing`, `:-preview.`, `:testing-`). Der Tag `:latest` ist strikt dem `main`-Workflow vorbehalten.
- **Secrets**:
- - `PACKAGE_TOKEN` (bzw. Fallback-Token-Namen wie `RELEASE_TOKEN`, `GITEA_TOKEN`) wird für API-Zugriffe auf Gitea Packages und Releases verwendet.
+ - `PACKAGE_TOKEN` (bzw. Fallback-Token-Namen wie `RELEASE_TOKEN`, `GITEA_TOKEN`) wird für API-Zugriffe auf Gitea Packages, Container Registry und Releases verwendet.
diff --git a/Cargo.lock b/Cargo.lock
index ad94a61..aa43e53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,5 +3,2128 @@
version = 4
[[package]]
-name = "rust-template"
-version = "0.0.0"
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc 0.2.189",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "config-ctdra"
+version = "1.0.4"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "f7b3e25b95d4cdb6fc856f2af0106d4558b882c2f10798db46181e51827749a4"
+dependencies = [
+ "confy",
+ "program-ctdra",
+ "serde",
+ "sudo-ctdra",
+]
+
+[[package]]
+name = "confy"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814"
+dependencies = [
+ "etcetera",
+ "lazy_static",
+ "serde",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc 0.2.189",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc 0.2.189",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc 0.2.189",
+ "r-efi",
+ "rand_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc 0.2.189",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libc"
+version = "1.0.0-alpha.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "logger-ctdra"
+version = "1.0.3"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "af9ea239d80163c80b12cae5f74c0c2824199921dedafba74678e96a11a8b15e"
+dependencies = [
+ "program-ctdra",
+ "time",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc 0.2.189",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "mirror-package"
+version = "1.0.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "config-ctdra",
+ "logger-ctdra",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc 0.2.189",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "program-ctdra"
+version = "1.0.0"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "54f20af67f90bf6d10697dd77bb4beb41dafad7fe0fb46aacfc81ad45a8e48bd"
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc 0.2.189",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc 0.2.189",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc 0.2.189",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc 0.2.189",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "sudo-ctdra"
+version = "1.0.0"
+source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
+checksum = "2c2746122bbe36f821de70db3ca4ad24edfb77118fc6a6bc7693082b50d23e5f"
+dependencies = [
+ "libc 1.0.0-alpha.4",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc 0.2.189",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "libc 0.2.189",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc 0.2.189",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "libc 0.2.189",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/Cargo.toml b/Cargo.toml
index 0cb0754..f985068 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,15 +1,22 @@
[package]
-name = "rust-template" # TODO: Projektnamen setzen
-version = "0.0.0" # TODO: Initiale Version setzen (z. B. 0.1.0)
+name = "mirror-package"
+version = "1.0.0"
edition = "2024"
-authors = ['DragonSlayer_14'] # TODO: Autor(en) anpassen
+authors = ['DragonSlayer_14']
readme = "README.md"
-license = "GPL-3.0-or-later" # TODO: Lizenz prüfen / anpassen
-repository = "https://gitea.creative-dragonslayer.de/Linuxapps/rust-template" # TODO: Repository-URL anpassen
-description = "" # TODO: Kurze Projektbeschreibung setzen
+license = "GPL-3.0-or-later"
+repository = "https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage"
+description = "Spiegelt vorkompilierte Linux-Pakete aus GitHub-Releases in Gitea-Paket-Registries"
[dependencies]
-# TODO: Projektabhängigkeiten hier hinzufügen
+config-ctdra = { version = "1.0.4", registry = "gitea" }
+logger-ctdra = { version = "1.0.3", registry = "gitea" }
+clap = { version = "4.6.6", features = ["derive", "env"] }
+reqwest = { version = "0.13.4", features = ["json", "stream"] }
+tokio = { version = "1.53.1", features = ["full"] }
+serde = { version = "1.0.229", features = ["derive"] }
+serde_json = "1.0.151"
+anyhow = "1.0.104"
[profile.release]
debug = "none"
@@ -17,28 +24,31 @@ debug = "none"
# --- Paketierungs-Metadaten für Linux-Distributionen ---
[package.metadata.deb]
-name = "" # TODO: Optional (Standard: package.name)
-maintainer = "" # TODO: Maintainer setzen (z. B. "Max Mustermann ")
-copyright = "" # TODO: Copyright-Hinweis setzen (z. B. "2026 Max Mustermann")
-section = "utils" # TODO: Debian-Sektion anpassen (z. B. utils, net, admin)
-priority = "optional" # TODO: Debian-Priorität anpassen (z. B. optional)
-depends = "$auto" # TODO: Abhängigkeiten definieren ($auto ermittelt via ldd automatisch)
+name = "mirror-package"
+maintainer = "DragonSlayer_14"
+copyright = "2026 DragonSlayer_14"
+section = "utils"
+priority = "optional"
+depends = "$auto, ca-certificates"
extended-description = """\
-""" # TODO: Ausführliche Paketbeschreibung setzen
+Spiegelt vorkompilierte Linux-Pakete (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea-Paket-Registries.\
+"""
assets = [
- # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
- # ["target/release/rust-template", "usr/bin/", "755"],
+ ["target/release/mirror-package", "usr/bin/mirror-package", "755"],
+ ["README.md", "usr/share/doc/mirror-package/README.md", "644"],
+ ["LICENSE", "usr/share/doc/mirror-package/copyright", "644"],
]
[package.metadata.generate-rpm]
assets = [
- # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
- # { source = "target/release/rust-template", dest = "/usr/bin/rust-template", mode = "755" },
+ { source = "target/release/mirror-package", dest = "/usr/bin/mirror-package", mode = "755" },
+ { source = "README.md", dest = "/usr/share/doc/mirror-package/README.md", mode = "644", doc = true },
+ { source = "LICENSE", dest = "/usr/share/licenses/mirror-package/LICENSE", mode = "644", license = true },
]
-requires = { } # TODO: RPM-Abhängigkeiten definieren, z. B. { "glibc" = "*" }
+requires = { "ca-certificates" = "*", "glibc" = "*" }
[package.metadata.arch]
-pkgrel = "1" # TODO: Standard Release-Nummer (wird in CI durch get-build-number.py dynamisch überschrieben)
-arch = "x86_64" # TODO: Zielarchitektur (z. B. x86_64, aarch64, i686)
-depends = ["gcc-libs", "glibc"] # TODO: Arch Linux Paketabhängigkeiten definieren
-optdepends = [] # TODO: Optionale Abhängigkeiten definieren (z. B. ["curl: für Web-Downloads"])
+pkgrel = "1"
+arch = "x86_64"
+depends = ["gcc-libs", "glibc", "ca-certificates"]
+optdepends = []
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d50ea7e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,37 @@
+# syntax=docker/dockerfile:1
+
+# Minimales und gehärtetes Runtime-Image
+FROM debian:bookworm-slim AS runtime
+
+# CA-Zertifikate und minimale dynamische Laufzeitbibliotheken installieren
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ ca-certificates \
+ libssl3 \
+ zlib1g \
+ libzstd1 && \
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/* && \
+ groupadd -g 10001 appuser && \
+ useradd -u 10001 -g appuser -d /home/appuser -m -s /usr/sbin/nologin appuser && \
+ mkdir -p /home/appuser/.config/mirror-package \
+ /home/appuser/.local/state/mirror-package \
+ /home/appuser/.local/share/mirror-package \
+ /etc/mirror-package && \
+ chown -R appuser:appuser /home/appuser /etc/mirror-package
+
+# Build-Argument mit Verweis auf die vorkompilierte Binary
+ARG TARGET_BIN=target/release/mirror-package
+COPY ${TARGET_BIN} /usr/local/bin/mirror-package
+
+# Strikte Ausführungsrechte setzen
+RUN chmod 0755 /usr/local/bin/mirror-package
+
+# Als unprivilegierter Benutzer ausführen
+USER 10001:10001
+WORKDIR /home/appuser
+
+# Volume-Mount-Punkte für Konfiguration und persistenten Status / Logs exponieren
+VOLUME ["/home/appuser/.config/mirror-package", "/home/appuser/.local/state/mirror-package"]
+
+ENTRYPOINT ["/usr/local/bin/mirror-package"]
+CMD ["--help"]
diff --git a/README.md b/README.md
index 56e1fe8..9777714 100644
--- a/README.md
+++ b/README.md
@@ -1,148 +1,253 @@
-# rust-template
+# mirror-package
-Ein modernes Template-Projekt für Rust-basierte Linux-Anwendungen und Kommandozeilen-Tools (CLI).
+Automatisiertes Werkzeug zum Extrahieren, Herunterladen und Spiegeln vorkompilierter Linux-Pakete aus GitHub-Releases in eine selbstgehostete Gitea- / Forgejo-Paket-Registry.
-Dieses Template bietet eine vorkonfigurierte Umgebung für modernes Rust (Edition 2024), automatisierte Multi-Architektur-Kompilierung und native Paketierung für die gängigsten Linux-Distributionen (Debian/Ubuntu, Fedora/RHEL, Arch Linux) sowie vollständige CI/CD-Pipelines für Gitea Actions (kompatibel mit Forgejo und GitHub Actions).
+`mirror-package` überwacht konfigurierte GitHub-Repositories (wie z. B. `raspberrypi/rpi-imager` oder `Heroic-Games-Launcher/HeroicGamesLauncher`), identifiziert vorkompilierte Linux-Pakete (`.deb`, `.rpm`, `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`) und veröffentlicht diese anhand von Release-Stabilitätsregeln automatisch in den passenden Distributionen der Gitea-Paket-Registry.
---
-## Features
+## Funktionen
-- **Rust Edition 2024**: Moderner Rust-Sprachstandard.
-- **Multi-Architektur-Kompilierung**:
- - `x86_64-unknown-linux-gnu` (64-Bit x86)
- - `aarch64-unknown-linux-gnu` (64-Bit ARM / ARM64)
- - `i686-unknown-linux-gnu` (32-Bit x86)
-- **Linux-Paketierung out-of-the-box**:
- - **Debian / Ubuntu** (`.deb` via `cargo-deb`)
- - **Fedora / RHEL / openSUSE** (`.rpm` via `cargo-generate-rpm`)
- - **Arch Linux** (`.pkg.tar.zst` via mitgeliefertem `scripts/package-arch.py`)
-- **Automatisierte CI/CD-Pipelines**:
- - `main`-Branch: Erstellt stabile Builds, ermittelt dynamisch Build-Nummern, paketiert für alle Architekturen, lädt Pakete in die Gitea Package Registry und erstellt Gitea Releases mit Dateianhängen.
- - `testing`-Branch: Erstellt Preview-Builds und Pre-Releases mit Testing-Paketen.
-- **Automatisierte Versions- & Build-Nummern**:
- - `scripts/get-build-number.py` fragt Gitea Releases, die Package Registry sowie lokale Artefakte ab, um Revisions-/Release-Nummern (z. B. `1.0.0-1`, `1.0.0-2`) automatisch zu erhöhen.
-- **Vorkonfigurierte Cargo-Einstellungen**:
- - `.cargo/config.toml` mit vorkonfigurierten Cross-Compilation-Linkern und Unterstützung für private/öffentliche Cargo Registries.
+- **Unterstützung mehrerer Distributionen**:
+ - **Debian / Ubuntu** (`.deb`)
+ - **Fedora / RHEL / openSUSE** (`.rpm`)
+ - **Arch Linux** (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`)
+- **Distributions-Routing-Regeln**:
+ - **Stabile Releases**: Debian-Pakete werden **sowohl** im `stable`- als auch im `testing`-Debian-Pool veröffentlicht (`pool/stable/main` und `pool/testing/main`); RPM-Pakete unter `rpm/stable`; Arch-Pakete unter `arch/stable`.
+ - **Pre-Releases**: Werden **ausschließlich** in Testing-Kanälen veröffentlicht: Debian `pool/testing/main`, RPM `rpm/testing` und Arch `arch/testing`.
+- **Persistente Konfiguration über `config-ctdra`**:
+ - Speichert Einstellungen automatisch im Benutzerpfad (`~/.config/mirror-package/config.toml`) oder Systempfad (`/etc/mirror-package/config.toml`).
+ - Das Hinzufügen oder Entfernen von Repositories über die CLI aktualisiert automatisch die Konfigurationsdatei.
+- **Flexible Authentifizierung**:
+ - GitHub-Releases können anonym abgefragt werden (kein Token erforderlich).
+ - Optionale Unterstützung für GitHub Personal Access Tokens zur Vermeidung von API-Rate-Limits.
+ - Gitea-Instanz-URL, API-Token und Registry-Owner können per CLI-Flags, Umgebungsvariablen oder Konfigurationsdatei festgelegt werden.
+- **Diagnose & Dry-Run-Modus**:
+ - Einheitliches Datei- und Konsolen-Logging über `logger-ctdra`.
+ - `--dry-run`-Flag zur Simulation von Download- und Upload-Schritten ohne Änderungen an Remote-Systemen vorzunehmen.
---
-## Struktur des Repositories
+## CLI-Verwendung & Befehle
+
+### 1. Konfiguration (`config`)
+Richte deine Gitea-Zugangsdaten und das optionale GitHub-Token ein:
+
+```bash
+# Gitea-Instanz und Registry-Owner konfigurieren
+mirror-package config set \
+ --gitea-url "https://gitea.creative-dragonslayer.de" \
+ --gitea-token "dein_gitea_api_token" \
+ --registry-owner "Linuxapps"
+
+# Optional: GitHub-Token für höhere API-Rate-Limits konfigurieren
+mirror-package config set --github-token "ghp_xxxxxxxxxxxx"
+
+# Aktuelle Konfiguration anzeigen
+mirror-package config show
+```
+
+Zugangsdaten können auch über Umgebungsvariablen (`GITEA_URL`, `GITEA_TOKEN`, `REGISTRY_OWNER`, `GITHUB_TOKEN`) oder globale CLI-Argumente übergeben werden.
+
+### 2. Überwachte Repositories verwalten (`add`, `remove`, `list`)
+
+```bash
+# Repositories zur persistenten Konfiguration hinzufügen
+mirror-package add raspberrypi/rpi-imager
+mirror-package add Heroic-Games-Launcher/HeroicGamesLauncher
+
+# Repository ohne Synchronisierung von Pre-Releases hinzufügen
+mirror-package add some-owner/some-repo --no-prereleases
+
+# Konfigurierte Repositories und deren Sync-Status auflisten
+mirror-package list
+
+# Ein Repository entfernen
+mirror-package remove raspberrypi/rpi-imager
+```
+
+### 3. Pakete synchronisieren (`sync`)
+
+```bash
+# Alle konfigurierten Repositories synchronisieren (neueste stabile Version und neuestes Pre-Release)
+mirror-package sync
+
+# Ein bestimmtes Repository synchronisieren
+mirror-package sync raspberrypi/rpi-imager
+
+# Alle historischen Releases eines Repositories synchronisieren
+mirror-package sync raspberrypi/rpi-imager --history
+
+# Dry-Run-Test (Simulation ohne Upload)
+mirror-package sync --dry-run
+```
+
+---
+
+## Docker & Container-Nutzung
+
+`mirror-package` wird als minimales, gehärtetes Container-Image in der Gitea Container Registry bereitgestellt.
+
+### 1. Image-Registry & Tagging-Strategie
+
+- **Stabile Releases (`main`-Branch)**:
+ - `//mirror-package:latest`
+ - `//mirror-package:`
+ - `//mirror-package:v`
+ - `//mirror-package:.`
+- **Testing- & Vorschau-Releases (`testing`-Branch)**:
+ - `//mirror-package:testing`
+ - `//mirror-package:-preview`
+ - `//mirror-package:-testing`
+ - `//mirror-package:-preview.`
+ - `//mirror-package:testing-`
+ - *(Hinweis: Der Tag `:latest` ist strikt dem stabilen `main`-Workflow vorbehalten).*
+
+### 2. Persistente Speicherpfade
+
+Um Konfigurationen, Status und Protokolle über Container-Neustarts hinweg beizubehalten, binde folgende Pfade an Host-Verzeichnisse oder Docker-Volumes:
+
+| Container-Pfad | Zweck | Empfohlener Mount-Typ |
+|---------------------------------------------|-------------------------------------------------------|------------------------------------|
+| `/home/appuser/.config/mirror-package` | Speichert `config.toml` (Zugangsdaten & Repo-Liste) | Host-Verzeichnis / Volume (rw) |
+| `/home/appuser/.local/state/mirror-package` | Persistente Anwendungsprotokolle & Statusdateien | Host-Verzeichnis / Volume (rw) |
+| `/tmp` | Download- & Streaming-Puffer für Paketdateien | `tmpfs` (RAM / tempfs, rw, noexec) |
+
+### 3. Sicherheitshärtung
+
+Der Container wurde nach höchsten Sicherheitsstandards aufgebaut:
+- **Unprivilegierter Benutzer**: Läuft als `appuser` (UID `10001`, GID `10001`), niemals als `root`.
+- **Read-Only Root-Dateisystem**: Voll funktionsfähig mit `--read-only` / `read_only: true`.
+- **Keine Capabilities**: Sämtliche Linux-Capabilities können sicher entzogen werden (`--cap-drop=ALL`).
+- **Keine Rechteausweitung**: Erzwingt `no-new-privileges:true`.
+- **Minimale Image-Größe**: Basiert auf Debian Bookworm Slim und enthält nur CA-Zertifikate und notwendige dynamische Bibliotheken (~40 MB).
+
+### 4. Ausführung über Docker CLI
+
+```bash
+# Zugangsdaten initialisieren / konfigurieren
+docker run --rm \
+ --name mirror-package \
+ --read-only \
+ --cap-drop=ALL \
+ --security-opt=no-new-privileges:true \
+ --tmpfs /tmp:rw,noexec,nosuid,size=2G \
+ -v "./config:/home/appuser/.config/mirror-package" \
+ -v "./state:/home/appuser/.local/state/mirror-package" \
+ gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest \
+ config set \
+ --gitea-url "https://gitea.creative-dragonslayer.de" \
+ --gitea-token "dein_token" \
+ --registry-owner "Linuxapps"
+
+# Repositories hinzufügen
+docker run --rm \
+ --read-only \
+ --cap-drop=ALL \
+ --security-opt=no-new-privileges:true \
+ --tmpfs /tmp:rw,noexec,nosuid,size=2G \
+ -v "./config:/home/appuser/.config/mirror-package" \
+ -v "./state:/home/appuser/.local/state/mirror-package" \
+ gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest \
+ add raspberrypi/rpi-imager
+
+# Synchronisation ausführen (kann über Cronjob oder Scheduler auf dem Host getriggert werden)
+docker run --rm \
+ --read-only \
+ --cap-drop=ALL \
+ --security-opt=no-new-privileges:true \
+ --tmpfs /tmp:rw,noexec,nosuid,size=2G \
+ -v "./config:/home/appuser/.config/mirror-package" \
+ -v "./state:/home/appuser/.local/state/mirror-package" \
+ gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest \
+ sync
+```
+
+### 5. Docker Compose Beispiel (`docker-compose.yml`)
+
+```yaml
+services:
+ mirror-package:
+ image: gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest
+ container_name: mirror-package
+ user: "10001:10001"
+ read_only: true
+ cap_drop:
+ - ALL
+ security_opt:
+ - no-new-privileges:true
+ volumes:
+ - ./config:/home/appuser/.config/mirror-package
+ - ./state:/home/appuser/.local/state/mirror-package
+ tmpfs:
+ - /tmp:rw,noexec,nosuid,size=2G
+ environment:
+ - GITEA_URL=https://gitea.creative-dragonslayer.de
+ - GITEA_TOKEN=dein_gitea_api_token
+ - REGISTRY_OWNER=Linuxapps
+ - GITHUB_TOKEN=dein_optionaler_github_pat
+ - LOG_LEVEL=info
+ command: ["sync"]
+```
+
+---
+
+## Repository-Struktur
```text
├── .cargo/
-│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry)
+│ └── config.toml # Linker- & Cargo-Konfiguration
├── .gitea/
│ └── workflows/
-│ ├── main.yaml # CI/CD-Workflow für stabile Releases (main-Branch)
-│ └── testing.yaml # CI/CD-Workflow für Pre-Releases (testing-Branch)
+│ ├── main.yaml # CI/CD: Release, Pakete & Container (Stable)
+│ └── testing.yaml # CI/CD: Preview, Pakete & Container (Testing)
├── scripts/
-│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
-│ └── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
+│ ├── get-build-number.py # Dynamische Ermittlung der Build-/Revisionsnummer
+│ └── package-arch.py # Erstellung nativer Arch Linux-Pakete
├── src/
-│ └── main.rs # Quellcode & Einstiegspunkt der Anwendung
-├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
-├── LICENSE # Lizenzdatei (Standard: GPL-3.0-or-later)
-├── AGENTS.md # Richtlinien und Leitfaden für KI-Coding-Agenten
+│ ├── main.rs # CLI-Einstiegspunkt & Befehlsausführung
+│ ├── lib.rs # Bibliotheks-Wurzel & Modulexporte
+│ ├── cli.rs # Clap-CLI-Argumente & Optionen
+│ ├── config.rs # config-ctdra Anbindung & Konfigurationsmodelle
+│ ├── gitea.rs # Gitea-Paket-Registry Upload-Client
+│ ├── github.rs # GitHub-API-Client & Paket-Klassifizierung
+│ └── pipeline.rs # End-to-End-Synchronisationspipeline & temporärer Speicher
+├── tests/
+│ ├── config_tests.rs # Tests für die Konfigurationsverwaltung
+│ ├── gitea_tests.rs # Tests für das Gitea-Upload-Routing
+│ └── github_tests.rs # Tests für Paket-Klassifizierung & Parsing
+├── .dockerignore # Ausschlussregeln für Container-Builds
+├── Dockerfile # Gehärtetes, minimales Runtime-Container-Image
+├── docker-compose.example.yml # Beispielkonfiguration für Docker Compose
+├── Cargo.toml # Projekt-Manifest und Paketierungs-Metadaten
+├── LICENSE # GPL-3.0-or-later Lizenztext
+├── AGENTS.md # Agenten- & Entwickler-Richtlinien
└── README.md # Projektdokumentation
```
---
-## Checkliste zur Verwendung als Template
-
-Wenn du ein neues Projekt aus diesem Template erstellst, gehe folgende Schritte durch:
-
-1. **`Cargo.toml` anpassen**:
- - `name`: Den Namen deiner Anwendung setzen.
- - `version`: Initiale Version festlegen (z. B. `0.1.0`).
- - `authors`, `repository`, `description`, `license`: Projektdaten eintragen.
- - Paketierungsabschnitte prüfen:
- - `[package.metadata.deb]`: `maintainer`, `copyright`, `section`, `extended-description` setzen.
- - `[package.metadata.generate-rpm]`: `requires` anpassen.
- - `[package.metadata.arch]`: `depends` und `optdepends` anpassen.
-2. **`src/` implementieren**:
- - Eigene Anwendungslogik in `src/main.rs` (bzw. Modulen / `src/lib.rs`) implementieren.
- - Tests in `src/` oder `tests/` ergänzen.
-3. **`.cargo/config.toml` prüfen**:
- - Falls crates.io statt einer privaten Registry genutzt werden soll, den Standard-Registry-Eintrag anpassen oder auskommentieren.
-4. **CI/CD Secrets konfigurieren**:
- - In den Repository-Einstellungen von Gitea/Forgejo ein Secret `PACKAGE_TOKEN` (bzw. `GITEA_TOKEN`) mit Rechten für Pakete und Releases hinterlegen.
-5. **Dokumentation aktualisieren**:
- - `README.md` an die konkrete Funktionsweise deiner Anwendung anpassen.
-
----
-
-## Lokale Entwicklung
+## Lokale Entwicklung & Bauen
### Voraussetzungen
+- **Rust & Cargo** (Stable Toolchain, Edition 2024 unterstützt)
+- **Python 3** (für Paketierungsskripte)
-- **Rust & Cargo** (aktuelle Stable-Version, Edition 2024 unterstützt)
-- **Python 3** (für Hilfsskripte in `scripts/`)
-- Für Cross-Compilation (optional):
- - `rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu i686-unknown-linux-gnu`
- - Cross-Toolchains: `gcc-aarch64-linux-gnu`, `gcc-i686-linux-gnu`
-- Für lokales Paketieren (optional):
- - `cargo install cargo-deb cargo-generate-rpm`
-
-### Wichtige Befehle
+### Befehle zum Bauen & Testen
```bash
-# Projekt bauen
-cargo build
+# Syntax- und Typprüfung ausführen
+cargo check
-# Tests ausführen
+# Unit-Tests ausführen
cargo test
-# Release-Build erstellen
+# Release-Binary kompilieren
cargo build --release
-
-# Cross-Compilation (Beispiel AArch64 / ARM64)
-cargo build --release --target aarch64-unknown-linux-gnu
-
-# Debian-Paket bauen (.deb)
-cargo deb
-
-# RPM-Paket bauen (.rpm)
-cargo generate-rpm
-
-# Arch Linux-Paket bauen (.pkg.tar.zst)
-python3 scripts/package-arch.py --arch x86_64 --pkgrel 1
```
---
-## Hilfsskripte
-
-### `scripts/get-build-number.py`
-Ermittelt dynamisch die nächste Build-/Paketnummer (pkgrel / revision).
-- Liest Name und Version automatisch aus `Cargo.toml`.
-- Fragt Gitea Releases und Package Registry ab, falls Token/URL vorhanden.
-- Berücksichtigt lokal im Verzeichnis `target/` liegende Pakete.
-- Aufruf: `python3 scripts/get-build-number.py`
-
-### `scripts/package-arch.py`
-Erstellt native Arch Linux-Pakete (`.pkg.tar.zst`), ohne dass `makepkg` oder eine Arch Linux-Installation erforderlich ist.
-- Liest Konfiguration aus `[package.metadata.arch]` in `Cargo.toml`.
-- Installiert die Binary nach `/usr/bin/`, sowie `LICENSE` und `README.md` nach `/usr/share/`.
-- Erzeugt eine standardkonforme `.PKGINFO`-Datei und komprimiert das Paket mit `zstandard`.
-- Parameter:
- - `--target`: Rust Target-Triple (z. B. `x86_64-unknown-linux-gnu`)
- - `--arch`: Zielarchitektur (z. B. `x86_64`, `aarch64`, `i686`)
- - `--pkgrel`: Release-Nummer
-
----
-
-## CI/CD Workflow-Übersicht
-
-| Branch | Workflow | Paket-Kanal | Release-Typ |
-| :--- | :--- | :--- | :--- |
-| `main` | `.gitea/workflows/main.yaml` | `stable` | Offizielles Release (`v`) |
-| `testing` | `.gitea/workflows/testing.yaml` | `testing` | Pre-Release (`v-preview`) |
-
----
-
## Lizenz
-Dieses Template steht standardmäßig unter der [GPL-3.0-or-later](LICENSE)-Lizenz. Die Lizenz kann bei Bedarf in `LICENSE` und `Cargo.toml` angepasst werden.
-
+Dieses Projekt ist unter der [GPL-3.0-or-later](LICENSE)-Lizenz lizenziert.
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
new file mode 100644
index 0000000..709e233
--- /dev/null
+++ b/docker-compose.example.yml
@@ -0,0 +1,24 @@
+services:
+ mirror-package:
+ image: gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest
+ container_name: mirror-package
+ # Sicherheit: Als unprivilegierter Benutzer ausführen, Read-Only-Dateisystem, Capabilities entziehen
+ user: "10001:10001"
+ read_only: true
+ cap_drop:
+ - ALL
+ security_opt:
+ - no-new-privileges:true
+ # Persistenter Speicher und temporärer Streaming-Puffer
+ volumes:
+ - ./config:/home/appuser/.config/mirror-package
+ - ./state:/home/appuser/.local/state/mirror-package
+ tmpfs:
+ - /tmp:rw,noexec,nosuid,size=2G
+ environment:
+ - GITEA_URL=https://gitea.creative-dragonslayer.de
+ - GITEA_TOKEN=dein_gitea_api_token
+ - REGISTRY_OWNER=Linuxapps
+ - GITHUB_TOKEN=dein_optionaler_github_pat
+ - LOG_LEVEL=info
+ command: ["sync"]
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..3cb120e
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,152 @@
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use std::path::PathBuf;
+
+/// Befehlszeilen-Parser für mirror-package.
+#[derive(Parser, Debug)]
+#[command(
+ name = "mirror-package",
+ author,
+ version,
+ about = "Spiegelt Linux-Pakete (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea-Paket-Registries",
+ long_about = None
+)]
+pub struct Cli {
+ /// Basis-URL der Gitea-Instanz (z. B. https://gitea.creative-dragonslayer.de)
+ #[arg(long, env = "GITEA_URL", global = true)]
+ pub gitea_url: Option,
+
+ /// Gitea-API-Token mit Schreibberechtigung für Pakete
+ #[arg(long, env = "GITEA_TOKEN", global = true)]
+ pub gitea_token: Option,
+
+ /// Gitea-Paket-Registry-Owner / Organisation
+ #[arg(long, env = "REGISTRY_OWNER", global = true)]
+ pub registry_owner: Option,
+
+ /// Optionales GitHub-Token für API-Anfragen (vermeidet Rate-Limits)
+ #[arg(long, env = "GITHUB_TOKEN", global = true)]
+ pub github_token: Option,
+
+ /// Benutzerdefinierter Pfad zur Konfigurationsdatei
+ #[arg(long, global = true)]
+ pub config: Option,
+
+ /// Führt einen Probelauf ohne Herunterladen oder Hochladen von Paketen durch
+ #[arg(long, global = true)]
+ pub dry_run: bool,
+
+ /// Logging-Level (error, warn, info, debug)
+ #[arg(long, value_enum, default_value_t = LogLevelArg::Info, global = true)]
+ pub log_level: LogLevelArg,
+
+ #[command(subcommand)]
+ pub command: Commands,
+}
+
+#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
+pub enum LogLevelArg {
+ Error,
+ Warn,
+ Info,
+ Debug,
+}
+
+impl From for logger_ctdra::LogLevel {
+ fn from(arg: LogLevelArg) -> Self {
+ match arg {
+ LogLevelArg::Error => logger_ctdra::LogLevel::Error,
+ LogLevelArg::Warn => logger_ctdra::LogLevel::Warn,
+ LogLevelArg::Info => logger_ctdra::LogLevel::Info,
+ LogLevelArg::Debug => logger_ctdra::LogLevel::Debug,
+ }
+ }
+}
+
+#[derive(Subcommand, Debug)]
+pub enum Commands {
+ /// Synchronisiert Pakete aus GitHub-Releases in die Gitea-Paket-Registry
+ Sync(SyncArgs),
+
+ /// Fügt ein GitHub-Repository zur persistenten Konfiguration hinzu
+ Add(AddArgs),
+
+ /// Entfernt ein GitHub-Repository aus der persistenten Konfiguration
+ #[command(alias = "rm")]
+ Remove(RemoveArgs),
+
+ /// Listet konfigurierte Repositories und deren Sync-Status auf
+ #[command(alias = "ls")]
+ List,
+
+ /// Zeigt die Anwendungskonfiguration an oder aktualisiert sie
+ Config(ConfigArgs),
+}
+
+#[derive(Args, Debug)]
+pub struct SyncArgs {
+ /// Bestimmter Repository-Name oder URL zur Synchronisation (z. B. "raspberrypi/rpi-imager"). Wenn weggelassen, werden alle konfigurierten Repositories synchronisiert.
+ pub repo: Option,
+
+ /// Alle konfigurierten Repositories synchronisieren
+ #[arg(short = 'a', long)]
+ pub all: bool,
+
+ /// Historische Releases scannen anstatt nur die neuesten Release(s)
+ #[arg(long)]
+ pub history: bool,
+
+ /// Pre-Releases während dieses Synchronisationslaufs einbeziehen (Standard: Repository-Konfiguration)
+ #[arg(long)]
+ pub prereleases: Option,
+}
+
+#[derive(Args, Debug)]
+pub struct AddArgs {
+ /// Zu spiegelndes GitHub-Repository (z. B. "raspberrypi/rpi-imager" oder vollständige GitHub-URL)
+ pub repo: String,
+
+ /// Keine Pre-Releases für dieses Repository spiegeln
+ #[arg(long)]
+ pub no_prereleases: bool,
+
+ /// Dieses Repository nach dem Hinzufügen sofort synchronisieren
+ #[arg(long)]
+ pub sync: bool,
+}
+
+#[derive(Args, Debug)]
+pub struct RemoveArgs {
+ /// Zu entfernendes Repository (z. B. "raspberrypi/rpi-imager")
+ pub repo: String,
+}
+
+#[derive(Args, Debug)]
+pub struct ConfigArgs {
+ #[command(subcommand)]
+ pub action: Option,
+}
+
+#[derive(Subcommand, Debug)]
+pub enum ConfigAction {
+ /// Aktuelle Konfiguration und Speicherort der Konfigurationsdatei anzeigen
+ Show,
+
+ /// Konfigurationsparameter setzen
+ Set {
+ /// Basis-URL der Gitea-Instanz
+ #[arg(long)]
+ gitea_url: Option,
+
+ /// Gitea-API-Token
+ #[arg(long)]
+ gitea_token: Option,
+
+ /// Gitea-Paket-Registry-Owner
+ #[arg(long)]
+ registry_owner: Option,
+
+ /// GitHub Personal Access Token
+ #[arg(long)]
+ github_token: Option,
+ },
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..40a537b
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,125 @@
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+fn default_true() -> bool {
+ true
+}
+
+/// Konfiguration für ein einzelnes zu spiegelndes Repository.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct RepoConfig {
+ /// Vollständiger Name des Repositories im Format "owner/repo".
+ pub name: String,
+ /// Gibt an, ob Pre-Releases gespiegelt werden sollen.
+ #[serde(default = "default_true")]
+ pub include_prereleases: bool,
+ /// Zuletzt erfolgreich synchronisierter Tag-Name.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub last_synced_tag: Option,
+}
+
+impl RepoConfig {
+ pub fn new(name: impl Into, include_prereleases: bool) -> Self {
+ Self {
+ name: name.into(),
+ include_prereleases,
+ last_synced_tag: None,
+ }
+ }
+}
+
+/// Persistente Anwendungskonfiguration.
+#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
+pub struct AppConfig {
+ /// Basis-URL der Gitea-Instanz (z. B. https://gitea.example.com).
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub gitea_url: Option,
+ /// Gitea-API-Authentifizierungstoken.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub gitea_token: Option,
+ /// Name des Gitea-Paket-Registry-Owners bzw. der Organisation.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub registry_owner: Option,
+ /// Optionales GitHub Personal Access Token für höhere Rate-Limits.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub github_token: Option,
+ /// Konfigurierte Repositories zur Spiegelung.
+ #[serde(default)]
+ pub repositories: Vec,
+}
+
+impl AppConfig {
+ /// Normalisiert die Repository-Eingabe, indem führende/nachgestellte Schrägstriche und GitHub-URL-Präfixe entfernt werden.
+ pub fn normalize_repo_name(input: &str) -> String {
+ let trimmed = input.trim();
+ let cleaned = trimmed
+ .trim_start_matches("https://github.com/")
+ .trim_start_matches("http://github.com/")
+ .trim_start_matches("github.com/")
+ .trim_end_matches(".git")
+ .trim_matches('/');
+ cleaned.to_string()
+ }
+
+ /// Fügt ein Repository zur Konfiguration hinzu oder aktualisiert ein bestehendes.
+ pub fn add_or_update_repo(&mut self, repo: RepoConfig) {
+ if let Some(existing) = self.repositories.iter_mut().find(|r| r.name.eq_ignore_ascii_case(&repo.name)) {
+ existing.include_prereleases = repo.include_prereleases;
+ } else {
+ self.repositories.push(repo);
+ }
+ }
+
+ /// Entfernt ein Repository anhand des Namens. Gibt true zurück, wenn es gefunden und entfernt wurde.
+ pub fn remove_repo(&mut self, repo_name: &str) -> bool {
+ let normalized = Self::normalize_repo_name(repo_name);
+ let before_len = self.repositories.len();
+ self.repositories.retain(|r| !r.name.eq_ignore_ascii_case(&normalized));
+ self.repositories.len() < before_len
+ }
+
+ /// Sucht ein Repository anhand des Namens.
+ pub fn find_repo(&self, repo_name: &str) -> Option<&RepoConfig> {
+ let normalized = Self::normalize_repo_name(repo_name);
+ self.repositories.iter().find(|r| r.name.eq_ignore_ascii_case(&normalized))
+ }
+
+ /// Aktualisiert den zuletzt synchronisierten Tag für ein bestimmtes Repository.
+ pub fn update_last_synced_tag(&mut self, repo_name: &str, tag: String) {
+ let normalized = Self::normalize_repo_name(repo_name);
+ if let Some(repo) = self.repositories.iter_mut().find(|r| r.name.eq_ignore_ascii_case(&normalized)) {
+ repo.last_synced_tag = Some(tag);
+ }
+ }
+}
+
+/// Setzt einen benutzerdefinierten Pfad für die Konfigurationsdatei, falls angegeben.
+pub fn init_config_path(custom_path: Option<&std::path::Path>) {
+ config_ctdra::set_config_name("config");
+ if let Some(path) = custom_path {
+ config_ctdra::set_custom_path(path.to_path_buf());
+ }
+}
+
+/// Gibt den aufgelösten Pfad der Konfigurationsdatei zurück.
+pub fn get_config_file_path() -> PathBuf {
+ config_ctdra::get_config_path()
+}
+
+/// Lädt die Anwendungskonfiguration. Verwendet Standardwerte, falls keine Datei vorhanden ist.
+pub fn load_config() -> AppConfig {
+ config_ctdra::load_config::()
+}
+
+/// Speichert die Anwendungskonfiguration auf der Festplatte.
+pub fn save_config(config: &AppConfig) -> Result<(), config_ctdra::ConfyError> {
+ config_ctdra::store(config)
+}
+
+/// Modifiziert die Anwendungskonfiguration atomar vor Ort und speichert sie.
+pub fn modify_config(f: F) -> Result
+where
+ F: FnOnce(&mut AppConfig),
+{
+ config_ctdra::modify::(f)
+}
diff --git a/src/gitea.rs b/src/gitea.rs
new file mode 100644
index 0000000..2de5ea3
--- /dev/null
+++ b/src/gitea.rs
@@ -0,0 +1,148 @@
+use crate::github::PackageType;
+use anyhow::{bail, Context, Result};
+use std::path::Path;
+use tokio::fs::File;
+
+/// Zugangsdaten und Ziel-Registry-Informationen für Gitea.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct GiteaConfig {
+ pub base_url: String,
+ pub token: String,
+ pub owner: String,
+}
+
+impl GiteaConfig {
+ pub fn new(base_url: impl Into, token: impl Into, owner: impl Into) -> Self {
+ let mut base_url = base_url.into();
+ while base_url.ends_with('/') {
+ base_url.pop();
+ }
+ Self {
+ base_url,
+ token: token.into(),
+ owner: owner.into(),
+ }
+ }
+}
+
+/// Ergebnis eines Paket-Upload-Versuchs.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum UploadStatus {
+ Uploaded,
+ AlreadyExists,
+ SimulatedDryRun,
+}
+
+/// Berechnet die Ziel-Upload-URLs der Gitea-Paket-Registry basierend auf Pakettyp und Release-Kanal.
+pub fn get_target_upload_urls(
+ base_url: &str,
+ owner: &str,
+ pkg_type: PackageType,
+ prerelease: bool,
+) -> Vec {
+ let base = base_url.trim_end_matches('/');
+ match pkg_type {
+ PackageType::Debian => {
+ if prerelease {
+ vec![format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner)]
+ } else {
+ vec![
+ format!("{}/api/packages/{}/debian/pool/stable/main/upload", base, owner),
+ format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner),
+ ]
+ }
+ }
+ PackageType::Rpm => {
+ if prerelease {
+ vec![format!("{}/api/packages/{}/rpm/testing/upload", base, owner)]
+ } else {
+ vec![format!("{}/api/packages/{}/rpm/stable/upload", base, owner)]
+ }
+ }
+ PackageType::Arch => {
+ if prerelease {
+ vec![format!("{}/api/packages/{}/arch/testing", base, owner)]
+ } else {
+ vec![format!("{}/api/packages/{}/arch/stable", base, owner)]
+ }
+ }
+ }
+}
+
+/// Client für den Upload von Paketen in eine Gitea-Paket-Registry.
+#[derive(Clone, Debug)]
+pub struct GiteaClient {
+ client: reqwest::Client,
+ config: GiteaConfig,
+}
+
+impl GiteaClient {
+ /// Erstellt einen neuen Gitea-Client.
+ pub fn new(config: GiteaConfig) -> Result {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert(
+ reqwest::header::USER_AGENT,
+ reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))),
+ );
+
+ let client = reqwest::Client::builder()
+ .default_headers(headers)
+ .build()
+ .context("Failed to initialize Gitea HTTP client")?;
+
+ Ok(Self { client, config })
+ }
+
+ /// Lädt eine Paketdatei von der Festplatte zum angegebenen Gitea-Endpunkt hoch.
+ pub async fn upload_file(
+ &self,
+ file_path: &Path,
+ endpoint_url: &str,
+ dry_run: bool,
+ ) -> Result {
+ if dry_run {
+ return Ok(UploadStatus::SimulatedDryRun);
+ }
+
+ let file = File::open(file_path)
+ .await
+ .with_context(|| format!("Failed to open package file {:?}", file_path))?;
+
+ let body = reqwest::Body::from(file);
+
+ let mut auth_header_val = format!("token {}", self.config.token);
+ if self.config.token.starts_with("Bearer ") || self.config.token.starts_with("token ") {
+ auth_header_val = self.config.token.clone();
+ }
+
+ let resp = self
+ .client
+ .put(endpoint_url)
+ .header(reqwest::header::AUTHORIZATION, auth_header_val)
+ .body(body)
+ .send()
+ .await
+ .with_context(|| format!("Failed to send PUT request to {}", endpoint_url))?;
+
+ let status = resp.status();
+ if status.is_success() {
+ return Ok(UploadStatus::Uploaded);
+ }
+
+ let error_body = resp.text().await.unwrap_or_default();
+ if status == reqwest::StatusCode::CONFLICT
+ || status == reqwest::StatusCode::UNPROCESSABLE_ENTITY
+ || error_body.to_ascii_lowercase().contains("already exists")
+ || error_body.to_ascii_lowercase().contains("duplicate")
+ {
+ return Ok(UploadStatus::AlreadyExists);
+ }
+
+ bail!(
+ "Failed to upload package to {} (HTTP {}): {}",
+ endpoint_url,
+ status,
+ error_body
+ );
+ }
+}
diff --git a/src/github.rs b/src/github.rs
new file mode 100644
index 0000000..03253d4
--- /dev/null
+++ b/src/github.rs
@@ -0,0 +1,259 @@
+use anyhow::{bail, Context, Result};
+use serde::{Deserialize, Serialize};
+
+/// Unterstützte Linux-Paketverteilungstypen.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum PackageType {
+ Debian,
+ Rpm,
+ Arch,
+}
+
+impl PackageType {
+ /// Klassifiziert ein Asset anhand seines Dateinamens in einen unterstützten PackageType.
+ pub fn from_filename(filename: &str) -> Option {
+ let name = filename.to_ascii_lowercase();
+ // Arch-Pakete aufgrund mehrteiliger Dateiendungen zuerst prüfen
+ if name.ends_with(".pkg.tar.zst")
+ || name.ends_with(".pkg.tar.xz")
+ || name.ends_with(".pkg.tar.gz")
+ || name.ends_with(".pacman")
+ {
+ Some(PackageType::Arch)
+ } else if name.ends_with(".deb") {
+ Some(PackageType::Debian)
+ } else if name.ends_with(".rpm") {
+ Some(PackageType::Rpm)
+ } else {
+ None
+ }
+ }
+
+ /// Gibt den lesbaren Namen des Pakettyps zurück.
+ pub fn name(&self) -> &'static str {
+ match self {
+ PackageType::Debian => "Debian (.deb)",
+ PackageType::Rpm => "Fedora/RPM (.rpm)",
+ PackageType::Arch => "Arch Linux (.pkg.tar.zst)",
+ }
+ }
+}
+
+/// Ein klassifiziertes Linux-Paket-Asset aus einem Release.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct ReleaseAsset {
+ pub name: String,
+ pub download_url: String,
+ pub size: u64,
+ pub package_type: PackageType,
+}
+
+/// Ein GitHub-Release mit extrahierten Linux-Paket-Assets.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct GitHubRelease {
+ pub id: u64,
+ pub tag_name: String,
+ pub name: Option,
+ pub prerelease: bool,
+ pub draft: bool,
+ pub assets: Vec,
+}
+
+#[derive(Deserialize, Debug)]
+struct GhApiRelease {
+ id: u64,
+ tag_name: String,
+ name: Option,
+ draft: bool,
+ prerelease: bool,
+ #[serde(default)]
+ assets: Vec,
+}
+
+#[derive(Deserialize, Debug)]
+struct GhApiAsset {
+ name: String,
+ browser_download_url: String,
+ size: u64,
+}
+
+/// Client für die Interaktion mit der GitHub-API.
+#[derive(Clone, Debug)]
+pub struct GitHubClient {
+ client: reqwest::Client,
+ token: Option,
+}
+
+impl GitHubClient {
+ /// Erstellt einen neuen GitHub-API-Client mit optionalem Authentifizierungstoken.
+ pub fn new(token: Option) -> Result {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert(
+ reqwest::header::USER_AGENT,
+ reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))),
+ );
+ headers.insert(
+ reqwest::header::ACCEPT,
+ reqwest::header::HeaderValue::from_static("application/vnd.github+json"),
+ );
+
+ let client = reqwest::Client::builder()
+ .default_headers(headers)
+ .build()
+ .context("Failed to initialize HTTP client")?;
+
+ Ok(Self { client, token })
+ }
+
+ /// Ruft Releases für ein angegebenes Repository ab (Format "owner/repo").
+ ///
+ /// Wenn `history` false ist, werden nur die neuesten Releases abgerufen.
+ /// Wenn `include_prereleases` false ist, werden Pre-Releases übersprungen.
+ pub async fn fetch_releases(
+ &self,
+ repo: &str,
+ history: bool,
+ include_prereleases: bool,
+ ) -> Result> {
+ let (owner, name) = parse_repo_owner_name(repo)?;
+
+ let mut releases = Vec::new();
+ let page_size = 30;
+ let mut page = 1;
+
+ loop {
+ let url = format!(
+ "https://api.github.com/repos/{}/{}/releases?per_page={}&page={}",
+ owner, name, page_size, page
+ );
+
+ let mut req = self.client.get(&url);
+ if let Some(token) = &self.token {
+ req = req.bearer_auth(token);
+ }
+
+ let resp = req.send().await.with_context(|| format!("Failed to send request to {}", url))?;
+ let status = resp.status();
+
+ if !status.is_success() {
+ let error_text = resp.text().await.unwrap_or_default();
+ bail!(
+ "GitHub API request failed for {}/{} (HTTP {}): {}",
+ owner,
+ name,
+ status,
+ error_text
+ );
+ }
+
+ let raw_releases: Vec = resp
+ .json()
+ .await
+ .with_context(|| format!("Failed to parse GitHub releases JSON for {}/{}", owner, name))?;
+
+ if raw_releases.is_empty() {
+ break;
+ }
+
+ for raw in raw_releases {
+ if raw.draft {
+ continue;
+ }
+ if raw.prerelease && !include_prereleases {
+ continue;
+ }
+
+ let filtered_assets: Vec = raw
+ .assets
+ .into_iter()
+ .filter_map(|a| {
+ PackageType::from_filename(&a.name).map(|pkg_type| ReleaseAsset {
+ name: a.name,
+ download_url: a.browser_download_url,
+ size: a.size,
+ package_type: pkg_type,
+ })
+ })
+ .collect();
+
+ releases.push(GitHubRelease {
+ id: raw.id,
+ tag_name: raw.tag_name,
+ name: raw.name,
+ prerelease: raw.prerelease,
+ draft: raw.draft,
+ assets: filtered_assets,
+ });
+ }
+
+ // Wenn kein vollständiger Verlauf gescannt werden soll, nach der ersten Seite stoppen
+ if !history {
+ // Nur das neueste stabile Release und neueste Pre-Release behalten (falls zulässig)
+ let mut selected = Vec::new();
+ let latest_stable = releases.iter().find(|r| !r.prerelease);
+ if let Some(stable) = latest_stable {
+ selected.push(stable.clone());
+ }
+
+ if include_prereleases {
+ let latest_prerelease = releases.iter().find(|r| r.prerelease);
+ if let Some(prerelease) = latest_prerelease {
+ // Duplikate vermeiden, falls Stable und Pre-Release identisch sind
+ if !selected.iter().any(|r| r.id == prerelease.id) {
+ selected.push(prerelease.clone());
+ }
+ }
+ }
+ return Ok(selected);
+ }
+
+ page += 1;
+ }
+
+ Ok(releases)
+ }
+
+ /// Lädt ein Asset von der angegebenen Download-URL als Streaming-Response herunter.
+ pub async fn download_asset_stream(&self, download_url: &str) -> Result {
+ let mut req = self.client.get(download_url);
+ if let Some(token) = &self.token {
+ req = req.bearer_auth(token);
+ }
+
+ let resp = req
+ .send()
+ .await
+ .with_context(|| format!("Failed to start download from {}", download_url))?;
+
+ if !resp.status().is_success() {
+ bail!(
+ "Download failed for {} (HTTP status {})",
+ download_url,
+ resp.status()
+ );
+ }
+
+ Ok(resp)
+ }
+}
+
+/// Hilfsfunktion zum Parsen von "owner/repo" aus einer Repository-Zeichenkette.
+pub fn parse_repo_owner_name(repo: &str) -> Result<(&str, &str)> {
+ let cleaned = repo
+ .trim()
+ .trim_start_matches("https://github.com/")
+ .trim_start_matches("http://github.com/")
+ .trim_start_matches("github.com/")
+ .trim_end_matches(".git")
+ .trim_matches('/');
+
+ let parts: Vec<&str> = cleaned.split('/').collect();
+ if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
+ bail!(
+ "Invalid repository format '{}'. Expected 'owner/repo' (e.g. 'raspberrypi/rpi-imager').",
+ repo
+ );
+ }
+
+ Ok((parts[0], parts[1]))
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..4d04728
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,8 @@
+//! `mirror-package` ist eine Anwendung zum Spiegeln vorkompilierter Linux-Pakete
+//! (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea- / Forgejo-Paket-Registries.
+
+pub mod cli;
+pub mod config;
+pub mod gitea;
+pub mod github;
+pub mod pipeline;
diff --git a/src/main.rs b/src/main.rs
index 79004d9..7c8176e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,197 @@
-fn main() {
- // TODO: Hauptlogik der Anwendung implementieren
- println!("Hello, World!");
-}
+use anyhow::Result;
+use clap::Parser;
+use logger_ctdra::{log, set_log_level, LogLevel};
+use mirror_package::cli::{Cli, Commands, ConfigAction};
+use mirror_package::config::{
+ get_config_file_path, init_config_path, load_config, modify_config, AppConfig, RepoConfig,
+};
+use mirror_package::pipeline::{sync_single_repository, SyncOptions, SyncReport};
-#[cfg(test)]
-mod tests {
- #[test]
- fn it_works() {
- // TODO: Unit-Tests für die Anwendung schreiben
- assert_eq!(2 + 2, 4);
+#[tokio::main]
+async fn main() -> Result<()> {
+ let cli = Cli::parse();
+
+ // Globalen Logging-Level initialisieren
+ set_log_level(cli.log_level.into());
+
+ // Benutzerdefinierten Konfigurationspfad initialisieren, falls angegeben
+ init_config_path(cli.config.as_deref());
+
+ // Persistente Konfiguration laden
+ let mut app_config = load_config();
+
+ // CLI-Flags überschreiben Konfigurationswerte
+ if let Some(url) = cli.gitea_url {
+ app_config.gitea_url = Some(url);
}
+ if let Some(token) = cli.gitea_token {
+ app_config.gitea_token = Some(token);
+ }
+ if let Some(owner) = cli.registry_owner {
+ app_config.registry_owner = Some(owner);
+ }
+ if let Some(gh_token) = cli.github_token {
+ app_config.github_token = Some(gh_token);
+ }
+
+ match cli.command {
+ Commands::Sync(sync_args) => {
+ let options = SyncOptions {
+ history: sync_args.history,
+ prereleases_override: sync_args.prereleases,
+ dry_run: cli.dry_run,
+ };
+
+ let repos_to_sync = if let Some(specific_repo) = sync_args.repo {
+ let norm = AppConfig::normalize_repo_name(&specific_repo);
+ let prereleases = sync_args.prereleases.unwrap_or(
+ app_config
+ .find_repo(&norm)
+ .map(|r| r.include_prereleases)
+ .unwrap_or(true),
+ );
+ vec![RepoConfig::new(norm, prereleases)]
+ } else if !app_config.repositories.is_empty() {
+ app_config.repositories.clone()
+ } else {
+ log(
+ "sync",
+ "No repositories configured. Add one with 'mirror-package add ' or specify a repository to sync.",
+ LogLevel::Warn,
+ );
+ return Ok(());
+ };
+
+ let mut report = SyncReport::default();
+
+ for repo in repos_to_sync {
+ if let Err(e) = sync_single_repository(&repo, &app_config, &options, &mut report).await {
+ log(
+ "sync",
+ &format!("Failed to sync repository '{}': {:#}", repo.name, e),
+ LogLevel::Error,
+ );
+ }
+ }
+
+ log(
+ "sync",
+ &format!(
+ "Sync finished: {} repository(ies) processed, {} package(s) uploaded, {} skipped (already exist), {} simulated (dry-run).",
+ report.repositories_processed,
+ report.packages_uploaded,
+ report.packages_already_exist,
+ report.packages_simulated
+ ),
+ LogLevel::Info,
+ );
+ }
+
+ Commands::Add(add_args) => {
+ let normalized_name = AppConfig::normalize_repo_name(&add_args.repo);
+ let include_prereleases = !add_args.no_prereleases;
+
+ let updated_repo = RepoConfig::new(&normalized_name, include_prereleases);
+ let to_add = updated_repo.clone();
+
+ modify_config(move |cfg| {
+ cfg.add_or_update_repo(to_add);
+ })?;
+
+ log(
+ "config",
+ &format!(
+ "Repository '{}' added (include pre-releases: {}).",
+ normalized_name, include_prereleases
+ ),
+ LogLevel::Info,
+ );
+
+ if add_args.sync {
+ let options = SyncOptions {
+ history: false,
+ prereleases_override: None,
+ dry_run: cli.dry_run,
+ };
+ let mut report = SyncReport::default();
+ sync_single_repository(&updated_repo, &app_config, &options, &mut report).await?;
+ }
+ }
+
+ Commands::Remove(rm_args) => {
+ let normalized_name = AppConfig::normalize_repo_name(&rm_args.repo);
+ let target_name = normalized_name.clone();
+
+ modify_config(move |cfg| {
+ cfg.remove_repo(&target_name);
+ })?;
+
+ log(
+ "config",
+ &format!("Repository '{}' removed from configuration.", normalized_name),
+ LogLevel::Info,
+ );
+ }
+
+ Commands::List => {
+ println!("Configured repositories (Config file: {:?}):", get_config_file_path());
+ if app_config.repositories.is_empty() {
+ println!(" (None configured yet. Use 'mirror-package add ' to add one.)");
+ } else {
+ println!("{:<40} {:<15} {:<20}", "REPOSITORY", "PRE-RELEASES", "LAST SYNCED TAG");
+ println!("{:-<40} {:-<15} {:-<20}", "", "", "");
+ for repo in &app_config.repositories {
+ println!(
+ "{:<40} {:<15} {:<20}",
+ repo.name,
+ if repo.include_prereleases { "yes" } else { "no" },
+ repo.last_synced_tag.as_deref().unwrap_or("-")
+ );
+ }
+ }
+ }
+
+ Commands::Config(config_args) => match config_args.action {
+ None | Some(ConfigAction::Show) => {
+ println!("Configuration location: {:?}", get_config_file_path());
+ println!("Gitea URL: {}", app_config.gitea_url.as_deref().unwrap_or("(not configured)"));
+ println!("Registry Owner: {}", app_config.registry_owner.as_deref().unwrap_or("(not configured)"));
+ println!(
+ "Gitea Token: {}",
+ if app_config.gitea_token.is_some() { "******** (set)" } else { "(not configured)" }
+ );
+ println!(
+ "GitHub Token: {}",
+ if app_config.github_token.is_some() { "******** (set)" } else { "(not configured, public access only)" }
+ );
+ println!("Repositories: {} configured", app_config.repositories.len());
+ }
+
+ Some(ConfigAction::Set {
+ gitea_url,
+ gitea_token,
+ registry_owner,
+ github_token,
+ }) => {
+ modify_config(move |cfg| {
+ if let Some(url) = gitea_url {
+ cfg.gitea_url = Some(url);
+ }
+ if let Some(tok) = gitea_token {
+ cfg.gitea_token = Some(tok);
+ }
+ if let Some(own) = registry_owner {
+ cfg.registry_owner = Some(own);
+ }
+ if let Some(gh) = github_token {
+ cfg.github_token = Some(gh);
+ }
+ })?;
+
+ log("config", "Configuration successfully updated.", LogLevel::Info);
+ }
+ },
+ }
+
+ Ok(())
}
diff --git a/src/pipeline.rs b/src/pipeline.rs
new file mode 100644
index 0000000..9b0e0d2
--- /dev/null
+++ b/src/pipeline.rs
@@ -0,0 +1,277 @@
+use crate::config::{modify_config, AppConfig, RepoConfig};
+use crate::gitea::{get_target_upload_urls, GiteaClient, GiteaConfig, UploadStatus};
+use crate::github::GitHubClient;
+use anyhow::{bail, Context, Result};
+use logger_ctdra::{log, LogLevel};
+use std::path::PathBuf;
+use std::time::{SystemTime, UNIX_EPOCH};
+use tokio::fs::{remove_file, File};
+use tokio::io::AsyncWriteExt;
+
+/// Zusammenfassende Statistiken eines Synchronisationsvorgangs.
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub struct SyncReport {
+ pub repositories_processed: usize,
+ pub releases_processed: usize,
+ pub packages_uploaded: usize,
+ pub packages_already_exist: usize,
+ pub packages_simulated: usize,
+}
+
+/// Optionen zur Steuerung eines Repository-Synchronisationslaufs.
+#[derive(Debug, Clone)]
+pub struct SyncOptions {
+ pub history: bool,
+ pub prereleases_override: Option,
+ pub dry_run: bool,
+}
+
+/// Führt die Synchronisations-Pipeline für ein bestimmtes Repository aus.
+pub async fn sync_single_repository(
+ repo_config: &RepoConfig,
+ app_config: &AppConfig,
+ options: &SyncOptions,
+ report: &mut SyncReport,
+) -> Result<()> {
+ let repo_name = &repo_config.name;
+ let include_prereleases = options
+ .prereleases_override
+ .unwrap_or(repo_config.include_prereleases);
+
+ log(
+ "sync",
+ &format!(
+ "Starting sync for repository '{}' (pre-releases: {}, history: {}, dry-run: {})",
+ repo_name, include_prereleases, options.history, options.dry_run
+ ),
+ LogLevel::Info,
+ );
+
+ // Gitea-Zugangsdaten auflösen
+ let gitea_url = app_config.gitea_url.as_deref().unwrap_or_default();
+ let gitea_token = app_config.gitea_token.as_deref().unwrap_or_default();
+ let registry_owner = app_config.registry_owner.as_deref().unwrap_or_default();
+
+ if !options.dry_run && (gitea_url.is_empty() || gitea_token.is_empty() || registry_owner.is_empty()) {
+ bail!(
+ "Missing Gitea configuration. Ensure --gitea-url, --gitea-token, and --registry-owner are provided or configured."
+ );
+ }
+
+ let github_client = GitHubClient::new(app_config.github_token.clone())
+ .context("Failed to initialize GitHub client")?;
+
+ let gitea_client = GiteaClient::new(GiteaConfig::new(
+ gitea_url,
+ gitea_token,
+ registry_owner,
+ ))
+ .context("Failed to initialize Gitea client")?;
+
+ let releases = github_client
+ .fetch_releases(repo_name, options.history, include_prereleases)
+ .await
+ .with_context(|| format!("Failed to fetch releases for repository '{}'", repo_name))?;
+
+ if releases.is_empty() {
+ log(
+ "sync",
+ &format!("No matching releases found for repository '{}'", repo_name),
+ LogLevel::Info,
+ );
+ report.repositories_processed += 1;
+ return Ok(());
+ }
+
+ log(
+ "sync",
+ &format!(
+ "Found {} release(s) to process for '{}'",
+ releases.len(),
+ repo_name
+ ),
+ LogLevel::Info,
+ );
+
+ let mut latest_synced_tag: Option = None;
+
+ for release in &releases {
+ report.releases_processed += 1;
+ let release_type_str = if release.prerelease { "pre-release" } else { "stable release" };
+
+ log(
+ "sync",
+ &format!(
+ "Processing {} '{}' (Tag: {}) with {} Linux package asset(s)",
+ release_type_str,
+ release.name.as_deref().unwrap_or(&release.tag_name),
+ release.tag_name,
+ release.assets.len()
+ ),
+ LogLevel::Info,
+ );
+
+ if release.assets.is_empty() {
+ log(
+ "sync",
+ &format!(
+ "No Linux packages (.deb, .rpm, .pkg.tar.zst) found in release {}",
+ release.tag_name
+ ),
+ LogLevel::Debug,
+ );
+ continue;
+ }
+
+ for asset in &release.assets {
+ let target_urls = get_target_upload_urls(
+ gitea_url,
+ registry_owner,
+ asset.package_type,
+ release.prerelease,
+ );
+
+ log(
+ "sync",
+ &format!(
+ "Mirroring asset '{}' ({}, size: {} bytes) -> {} endpoint(s)",
+ asset.name,
+ asset.package_type.name(),
+ asset.size,
+ target_urls.len()
+ ),
+ LogLevel::Info,
+ );
+
+ if options.dry_run {
+ for url in &target_urls {
+ log(
+ "dry-run",
+ &format!("[DRY-RUN] Would upload '{}' to '{}'", asset.name, url),
+ LogLevel::Info,
+ );
+ }
+ report.packages_simulated += target_urls.len();
+ continue;
+ }
+
+ // Asset in temporäre Datei herunterladen
+ let temp_file_path = download_to_temp_file(&github_client, &asset.download_url, &asset.name)
+ .await
+ .with_context(|| format!("Failed to download asset '{}'", asset.name))?;
+
+ // Asset auf alle Ziel-Distributions-URLs hochladen
+ for url in &target_urls {
+ log(
+ "upload",
+ &format!("Uploading '{}' to '{}'...", asset.name, url),
+ LogLevel::Debug,
+ );
+
+ match gitea_client.upload_file(&temp_file_path, url, options.dry_run).await {
+ Ok(UploadStatus::Uploaded) => {
+ log(
+ "upload",
+ &format!("Successfully uploaded '{}' to {}", asset.name, url),
+ LogLevel::Info,
+ );
+ report.packages_uploaded += 1;
+ }
+ Ok(UploadStatus::AlreadyExists) => {
+ log(
+ "upload",
+ &format!(
+ "Package '{}' already exists in registry ({}), skipping duplicate.",
+ asset.name, url
+ ),
+ LogLevel::Warn,
+ );
+ report.packages_already_exist += 1;
+ }
+ Ok(UploadStatus::SimulatedDryRun) => {
+ report.packages_simulated += 1;
+ }
+ Err(e) => {
+ log(
+ "upload",
+ &format!("Failed to upload '{}' to {}: {:#}", asset.name, url, e),
+ LogLevel::Error,
+ );
+ // Temporäre Datei vor Fehlerrückgabe bereinigen
+ let _ = remove_file(&temp_file_path).await;
+ return Err(e);
+ }
+ }
+ }
+
+ // Temporäre Datei bereinigen
+ if let Err(e) = remove_file(&temp_file_path).await {
+ log(
+ "sync",
+ &format!("Failed to clean up temp file {:?}: {}", temp_file_path, e),
+ LogLevel::Warn,
+ );
+ }
+ }
+
+ if latest_synced_tag.is_none() {
+ latest_synced_tag = Some(release.tag_name.clone());
+ }
+ }
+
+ // Zuletzt synchronisierten Tag persistieren, wenn kein Dry-Run
+ if !options.dry_run {
+ if let Some(tag) = latest_synced_tag {
+ let name_copy = repo_name.to_string();
+ let _ = modify_config(move |cfg| {
+ cfg.update_last_synced_tag(&name_copy, tag);
+ });
+ }
+ }
+
+ report.repositories_processed += 1;
+ Ok(())
+}
+
+/// Lädt ein GitHub-Release-Asset in eine eindeutige temporäre Datei herunter.
+async fn download_to_temp_file(
+ github_client: &GitHubClient,
+ download_url: &str,
+ filename: &str,
+) -> Result {
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_millis();
+
+ let temp_dir = std::env::temp_dir().join("mirror-package");
+ tokio::fs::create_dir_all(&temp_dir)
+ .await
+ .with_context(|| format!("Failed to create temp directory {:?}", temp_dir))?;
+
+ let safe_filename = format!("{}-{}", now, filename.replace(['/', '\\'], "_"));
+ let temp_path = temp_dir.join(safe_filename);
+
+ log(
+ "download",
+ &format!("Downloading '{}' to temporary path {:?}...", filename, temp_path),
+ LogLevel::Debug,
+ );
+
+ let mut response = github_client.download_asset_stream(download_url).await?;
+ let mut file = File::create(&temp_path)
+ .await
+ .with_context(|| format!("Failed to create temp file {:?}", temp_path))?;
+
+ while let Some(chunk) = response.chunk().await.context("Error reading download stream")? {
+ file.write_all(&chunk)
+ .await
+ .context("Error writing chunk to temp file")?;
+ }
+
+ file.flush()
+ .await
+ .context("Error flushing temp file to disk")?;
+
+ Ok(temp_path)
+}
diff --git a/tests/config_tests.rs b/tests/config_tests.rs
new file mode 100644
index 0000000..c844d4b
--- /dev/null
+++ b/tests/config_tests.rs
@@ -0,0 +1,53 @@
+use mirror_package::config::{AppConfig, RepoConfig};
+
+#[test]
+fn test_normalize_repo_name() {
+ assert_eq!(
+ AppConfig::normalize_repo_name("raspberrypi/rpi-imager"),
+ "raspberrypi/rpi-imager"
+ );
+ assert_eq!(
+ AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager"),
+ "raspberrypi/rpi-imager"
+ );
+ assert_eq!(
+ AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager.git"),
+ "raspberrypi/rpi-imager"
+ );
+ assert_eq!(
+ AppConfig::normalize_repo_name("Heroic-Games-Launcher/HeroicGamesLauncher/"),
+ "Heroic-Games-Launcher/HeroicGamesLauncher"
+ );
+}
+
+#[test]
+fn test_repo_management() {
+ let mut config = AppConfig::default();
+ config.add_or_update_repo(RepoConfig::new("owner/repo1", true));
+ config.add_or_update_repo(RepoConfig::new("owner/repo2", false));
+
+ assert_eq!(config.repositories.len(), 2);
+ assert!(config.find_repo("owner/repo1").is_some());
+ assert!(config.find_repo("https://github.com/owner/repo1").is_some());
+
+ assert!(config.remove_repo("owner/repo1"));
+ assert_eq!(config.repositories.len(), 1);
+ assert!(config.find_repo("owner/repo1").is_none());
+}
+
+#[test]
+fn test_serialization_roundtrip() {
+ let mut config = AppConfig {
+ gitea_url: Some("https://gitea.example.com".to_string()),
+ gitea_token: Some("secret_token".to_string()),
+ registry_owner: Some("my-org".to_string()),
+ github_token: Some("gh_pat".to_string()),
+ repositories: Vec::new(),
+ };
+ config.add_or_update_repo(RepoConfig::new("raspberrypi/rpi-imager", true));
+
+ let json_str = serde_json::to_string(&config).unwrap();
+ let deserialized: AppConfig = serde_json::from_str(&json_str).unwrap();
+
+ assert_eq!(config, deserialized);
+}
diff --git a/tests/gitea_tests.rs b/tests/gitea_tests.rs
new file mode 100644
index 0000000..06b8b86
--- /dev/null
+++ b/tests/gitea_tests.rs
@@ -0,0 +1,65 @@
+use mirror_package::gitea::get_target_upload_urls;
+use mirror_package::github::PackageType;
+
+#[test]
+fn test_debian_routing() {
+ let base_url = "https://gitea.example.com";
+ let owner = "test-owner";
+
+ // Stabile Debian-Pakete werden in stable und testing Pools hochgeladen
+ let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Debian, false);
+ assert_eq!(
+ stable_urls,
+ vec![
+ "https://gitea.example.com/api/packages/test-owner/debian/pool/stable/main/upload",
+ "https://gitea.example.com/api/packages/test-owner/debian/pool/testing/main/upload",
+ ]
+ );
+
+ // Pre-Release Debian-Pakete gehen nur in den testing Pool
+ let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Debian, true);
+ assert_eq!(
+ prerelease_urls,
+ vec!["https://gitea.example.com/api/packages/test-owner/debian/pool/testing/main/upload"]
+ );
+}
+
+#[test]
+fn test_rpm_routing() {
+ let base_url = "https://gitea.example.com/";
+ let owner = "test-owner";
+
+ // Stabiles RPM
+ let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Rpm, false);
+ assert_eq!(
+ stable_urls,
+ vec!["https://gitea.example.com/api/packages/test-owner/rpm/stable/upload"]
+ );
+
+ // Pre-Release RPM
+ let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Rpm, true);
+ assert_eq!(
+ prerelease_urls,
+ vec!["https://gitea.example.com/api/packages/test-owner/rpm/testing/upload"]
+ );
+}
+
+#[test]
+fn test_arch_routing() {
+ let base_url = "https://gitea.example.com";
+ let owner = "test-owner";
+
+ // Stabiles Arch
+ let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, false);
+ assert_eq!(
+ stable_urls,
+ vec!["https://gitea.example.com/api/packages/test-owner/arch/stable"]
+ );
+
+ // Pre-Release Arch
+ let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, true);
+ assert_eq!(
+ prerelease_urls,
+ vec!["https://gitea.example.com/api/packages/test-owner/arch/testing"]
+ );
+}
diff --git a/tests/github_tests.rs b/tests/github_tests.rs
new file mode 100644
index 0000000..1a3b4f4
--- /dev/null
+++ b/tests/github_tests.rs
@@ -0,0 +1,70 @@
+use mirror_package::github::{parse_repo_owner_name, PackageType};
+
+#[test]
+fn test_package_classification() {
+ // Debian
+ assert_eq!(
+ PackageType::from_filename("rpi-imager_1.8.5_amd64.deb"),
+ Some(PackageType::Debian)
+ );
+ assert_eq!(
+ PackageType::from_filename("heroic_2.14.1_arm64.DEB"),
+ Some(PackageType::Debian)
+ );
+
+ // RPM
+ assert_eq!(
+ PackageType::from_filename("rpi-imager-1.8.5-1.x86_64.rpm"),
+ Some(PackageType::Rpm)
+ );
+ assert_eq!(
+ PackageType::from_filename("heroic-2.14.1.aarch64.RPM"),
+ Some(PackageType::Rpm)
+ );
+
+ // Arch Linux
+ assert_eq!(
+ PackageType::from_filename("heroic-games-launcher-bin-2.14.1-1-x86_64.pkg.tar.zst"),
+ Some(PackageType::Arch)
+ );
+ assert_eq!(
+ PackageType::from_filename("rpi-imager-1.8.5-1-x86_64.pkg.tar.xz"),
+ Some(PackageType::Arch)
+ );
+ assert_eq!(
+ PackageType::from_filename("package-1.0.0-1-any.pkg.tar.gz"),
+ Some(PackageType::Arch)
+ );
+ assert_eq!(
+ PackageType::from_filename("Heroic-2.14.1.pacman"),
+ Some(PackageType::Arch)
+ );
+
+ // Nicht unterstützte Paketformate sollten ignoriert werden
+ assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.AppImage"), None);
+ assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.dmg"), None);
+ assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.exe"), None);
+ assert_eq!(PackageType::from_filename("source-code.tar.gz"), None);
+ assert_eq!(PackageType::from_filename("source-code.zip"), None);
+ assert_eq!(PackageType::from_filename("checksums.sha256"), None);
+ assert_eq!(PackageType::from_filename("release.sig"), None);
+}
+
+#[test]
+fn test_parse_repo_owner_name() {
+ assert_eq!(
+ parse_repo_owner_name("raspberrypi/rpi-imager").unwrap(),
+ ("raspberrypi", "rpi-imager")
+ );
+ assert_eq!(
+ parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher").unwrap(),
+ ("Heroic-Games-Launcher", "HeroicGamesLauncher")
+ );
+ assert_eq!(
+ parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher.git").unwrap(),
+ ("Heroic-Games-Launcher", "HeroicGamesLauncher")
+ );
+
+ assert!(parse_repo_owner_name("invalid_repo").is_err());
+ assert!(parse_repo_owner_name("invalid/repo/extra").is_err());
+}