Merge pull request 'Merge testing in main: Release 1.0.0' (#7) from testing into main
Main Release & Publish / Build, Publish Packages (Stable) & Create Release (push) Successful in 9m5s
Main Release & Publish / Build, Publish Packages (Stable) & Create Release (push) Successful in 9m5s
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
!target/release/mirror-package
|
||||||
|
!target/x86_64-unknown-linux-gnu/release/mirror-package
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitea/
|
||||||
|
tests/
|
||||||
@@ -181,3 +181,34 @@ jobs:
|
|||||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
||||||
echo "Asset ${filename} erfolgreich hochgeladen."
|
echo "Asset ${filename} erfolgreich hochgeladen."
|
||||||
done
|
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."
|
||||||
|
|||||||
@@ -181,3 +181,36 @@ jobs:
|
|||||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
||||||
echo "Asset ${filename} erfolgreich hochgeladen."
|
echo "Asset ${filename} erfolgreich hochgeladen."
|
||||||
done
|
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."
|
||||||
|
|||||||
Generated
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||||
|
<state>
|
||||||
|
<map>
|
||||||
|
<entry key="GitHub Workflow">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="GitHub Workflow" />
|
||||||
|
<option name="relativePathToSchema" value="https://www.schemastore.org/github-workflow.json" />
|
||||||
|
<option name="applicationDefined" value="true" />
|
||||||
|
<option name="patterns">
|
||||||
|
<list>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/main.yaml" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/testing.yaml" />
|
||||||
|
</Item>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+1
@@ -3,6 +3,7 @@
|
|||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
# AGENTS.md
|
# 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
|
## 1. Projektübersicht & Philosophie
|
||||||
|
|
||||||
Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
|
`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.
|
||||||
- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`).
|
|
||||||
- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`).
|
### Kernfunktionen & Routing
|
||||||
- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
|
- **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
|
### Standards & Tech-Stack
|
||||||
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
|
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
|
||||||
- **Rust Toolchain**: Stable.
|
- **Rust Toolchain**: Stable.
|
||||||
- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`).
|
- **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
|
│ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer
|
||||||
│ └── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
|
│ └── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
|
||||||
├── src/
|
├── 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
|
├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch
|
||||||
├── LICENSE # Lizenztext
|
├── LICENSE # Lizenztext
|
||||||
├── README.md # Benutzerdokumentation & Setup-Checkliste
|
├── README.md # Benutzerdokumentation
|
||||||
└── AGENTS.md # Dieses Agenten-Handbuch
|
└── 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
|
### 3.1 Code-Stil & Best Practices
|
||||||
- **Idiomatisches Rust**: Nutze moderne Sprachfeatures der Rust Edition 2024.
|
- **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.
|
- **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. Behalte die bestehende Sprachkonvention bei.
|
- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen.
|
||||||
- **Template-TODOs**: Wenn neue Vorlagen-Features oder Platzhalter ergänzt werden, markiere anpassungsbedürftige Stellen eindeutig mit `// TODO:` (Rust), `# TODO:` (TOML/Python/YAML).
|
- **Logging**: Nutze `logger-ctdra` für strukturierte Konsolen- und Datei-Logs.
|
||||||
|
|
||||||
### 3.2 Paketierungs-Metadaten in `Cargo.toml`
|
### 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:
|
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`.
|
- `pkgrel`, `arch`, `depends`, `optdepends`.
|
||||||
|
|
||||||
### 3.3 Skripte in `scripts/`
|
### 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.
|
- **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: `json`, `subprocess`, `urllib`, `argparse`, `os`, `re`, `tempfile`, `tarfile` etc.).
|
- **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
|
## 4. Workflows: Bauen, Testen & Validieren
|
||||||
|
|
||||||
Agenten müssen Änderungen vor dem Abschluss validieren.
|
|
||||||
|
|
||||||
### 4.1 Grundlegende Validierung
|
### 4.1 Grundlegende Validierung
|
||||||
```bash
|
```bash
|
||||||
# Syntax- und Typprüfung
|
# Syntax- und Typprüfung
|
||||||
@@ -94,7 +120,7 @@ python3 scripts/package-arch.py --arch x86_64 --pkgrel 1
|
|||||||
## 5. CI/CD-Pipeline Details
|
## 5. CI/CD-Pipeline Details
|
||||||
|
|
||||||
- **Trigger**:
|
- **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<VERSION>`.
|
- `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<VERSION>` und baut/veröffentlicht das Docker-Container-Image mit den Tags `:latest`, `:<VERSION>`, `:v<VERSION>` und `:<VERSION>.<BUILD_NUMBER>`.
|
||||||
- `push` auf `testing`: Baut Binaries & Pakete für den `testing`-Kanal und erstellt ein Pre-Release `v<VERSION>-preview`.
|
- `push` auf `testing`: Baut Binaries & Pakete für den `testing`-Kanal, erstellt ein Pre-Release `v<VERSION>-preview` und baut/veröffentlicht das Docker-Container-Image ausschließlich mit eindeutigen Testing-Tags (`:testing`, `:<VERSION>-preview`, `:<VERSION>-testing`, `:<VERSION>-preview.<BUILD_NUMBER>`, `:testing-<BUILD_NUMBER>`). Der Tag `:latest` ist strikt dem `main`-Workflow vorbehalten.
|
||||||
- **Secrets**:
|
- **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.
|
||||||
|
|||||||
Generated
+2125
-2
File diff suppressed because it is too large
Load Diff
+33
-23
@@ -1,15 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-template" # TODO: Projektnamen setzen
|
name = "mirror-package"
|
||||||
version = "0.0.0" # TODO: Initiale Version setzen (z. B. 0.1.0)
|
version = "1.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ['DragonSlayer_14'] # TODO: Autor(en) anpassen
|
authors = ['DragonSlayer_14']
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later" # TODO: Lizenz prüfen / anpassen
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://gitea.creative-dragonslayer.de/Linuxapps/rust-template" # TODO: Repository-URL anpassen
|
repository = "https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage"
|
||||||
description = "" # TODO: Kurze Projektbeschreibung setzen
|
description = "Spiegelt vorkompilierte Linux-Pakete aus GitHub-Releases in Gitea-Paket-Registries"
|
||||||
|
|
||||||
[dependencies]
|
[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]
|
[profile.release]
|
||||||
debug = "none"
|
debug = "none"
|
||||||
@@ -17,28 +24,31 @@ debug = "none"
|
|||||||
# --- Paketierungs-Metadaten für Linux-Distributionen ---
|
# --- Paketierungs-Metadaten für Linux-Distributionen ---
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
name = "" # TODO: Optional (Standard: package.name)
|
name = "mirror-package"
|
||||||
maintainer = "" # TODO: Maintainer setzen (z. B. "Max Mustermann <max@example.com>")
|
maintainer = "DragonSlayer_14"
|
||||||
copyright = "" # TODO: Copyright-Hinweis setzen (z. B. "2026 Max Mustermann")
|
copyright = "2026 DragonSlayer_14"
|
||||||
section = "utils" # TODO: Debian-Sektion anpassen (z. B. utils, net, admin)
|
section = "utils"
|
||||||
priority = "optional" # TODO: Debian-Priorität anpassen (z. B. optional)
|
priority = "optional"
|
||||||
depends = "$auto" # TODO: Abhängigkeiten definieren ($auto ermittelt via ldd automatisch)
|
depends = "$auto, ca-certificates"
|
||||||
extended-description = """\
|
extended-description = """\
|
||||||
""" # TODO: Ausführliche Paketbeschreibung setzen
|
Spiegelt vorkompilierte Linux-Pakete (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea-Paket-Registries.\
|
||||||
|
"""
|
||||||
assets = [
|
assets = [
|
||||||
# TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
|
["target/release/mirror-package", "usr/bin/mirror-package", "755"],
|
||||||
# ["target/release/rust-template", "usr/bin/", "755"],
|
["README.md", "usr/share/doc/mirror-package/README.md", "644"],
|
||||||
|
["LICENSE", "usr/share/doc/mirror-package/copyright", "644"],
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
assets = [
|
assets = [
|
||||||
# TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.:
|
{ source = "target/release/mirror-package", dest = "/usr/bin/mirror-package", mode = "755" },
|
||||||
# { source = "target/release/rust-template", dest = "/usr/bin/rust-template", 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]
|
[package.metadata.arch]
|
||||||
pkgrel = "1" # TODO: Standard Release-Nummer (wird in CI durch get-build-number.py dynamisch überschrieben)
|
pkgrel = "1"
|
||||||
arch = "x86_64" # TODO: Zielarchitektur (z. B. x86_64, aarch64, i686)
|
arch = "x86_64"
|
||||||
depends = ["gcc-libs", "glibc"] # TODO: Arch Linux Paketabhängigkeiten definieren
|
depends = ["gcc-libs", "glibc", "ca-certificates"]
|
||||||
optdepends = [] # TODO: Optionale Abhängigkeiten definieren (z. B. ["curl: für Web-Downloads"])
|
optdepends = []
|
||||||
|
|||||||
+37
@@ -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"]
|
||||||
@@ -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.
|
- **Unterstützung mehrerer Distributionen**:
|
||||||
- **Multi-Architektur-Kompilierung**:
|
- **Debian / Ubuntu** (`.deb`)
|
||||||
- `x86_64-unknown-linux-gnu` (64-Bit x86)
|
- **Fedora / RHEL / openSUSE** (`.rpm`)
|
||||||
- `aarch64-unknown-linux-gnu` (64-Bit ARM / ARM64)
|
- **Arch Linux** (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`)
|
||||||
- `i686-unknown-linux-gnu` (32-Bit x86)
|
- **Distributions-Routing-Regeln**:
|
||||||
- **Linux-Paketierung out-of-the-box**:
|
- **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`.
|
||||||
- **Debian / Ubuntu** (`.deb` via `cargo-deb`)
|
- **Pre-Releases**: Werden **ausschließlich** in Testing-Kanälen veröffentlicht: Debian `pool/testing/main`, RPM `rpm/testing` und Arch `arch/testing`.
|
||||||
- **Fedora / RHEL / openSUSE** (`.rpm` via `cargo-generate-rpm`)
|
- **Persistente Konfiguration über `config-ctdra`**:
|
||||||
- **Arch Linux** (`.pkg.tar.zst` via mitgeliefertem `scripts/package-arch.py`)
|
- Speichert Einstellungen automatisch im Benutzerpfad (`~/.config/mirror-package/config.toml`) oder Systempfad (`/etc/mirror-package/config.toml`).
|
||||||
- **Automatisierte CI/CD-Pipelines**:
|
- Das Hinzufügen oder Entfernen von Repositories über die CLI aktualisiert automatisch die Konfigurationsdatei.
|
||||||
- `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.
|
- **Flexible Authentifizierung**:
|
||||||
- `testing`-Branch: Erstellt Preview-Builds und Pre-Releases mit Testing-Paketen.
|
- GitHub-Releases können anonym abgefragt werden (kein Token erforderlich).
|
||||||
- **Automatisierte Versions- & Build-Nummern**:
|
- Optionale Unterstützung für GitHub Personal Access Tokens zur Vermeidung von API-Rate-Limits.
|
||||||
- `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.
|
- Gitea-Instanz-URL, API-Token und Registry-Owner können per CLI-Flags, Umgebungsvariablen oder Konfigurationsdatei festgelegt werden.
|
||||||
- **Vorkonfigurierte Cargo-Einstellungen**:
|
- **Diagnose & Dry-Run-Modus**:
|
||||||
- `.cargo/config.toml` mit vorkonfigurierten Cross-Compilation-Linkern und Unterstützung für private/öffentliche Cargo Registries.
|
- 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)**:
|
||||||
|
- `<registry>/<owner>/mirror-package:latest`
|
||||||
|
- `<registry>/<owner>/mirror-package:<version>`
|
||||||
|
- `<registry>/<owner>/mirror-package:v<version>`
|
||||||
|
- `<registry>/<owner>/mirror-package:<version>.<build_number>`
|
||||||
|
- **Testing- & Vorschau-Releases (`testing`-Branch)**:
|
||||||
|
- `<registry>/<owner>/mirror-package:testing`
|
||||||
|
- `<registry>/<owner>/mirror-package:<version>-preview`
|
||||||
|
- `<registry>/<owner>/mirror-package:<version>-testing`
|
||||||
|
- `<registry>/<owner>/mirror-package:<version>-preview.<build_number>`
|
||||||
|
- `<registry>/<owner>/mirror-package:testing-<build_number>`
|
||||||
|
- *(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
|
```text
|
||||||
├── .cargo/
|
├── .cargo/
|
||||||
│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry)
|
│ └── config.toml # Linker- & Cargo-Konfiguration
|
||||||
├── .gitea/
|
├── .gitea/
|
||||||
│ └── workflows/
|
│ └── workflows/
|
||||||
│ ├── main.yaml # CI/CD-Workflow für stabile Releases (main-Branch)
|
│ ├── main.yaml # CI/CD: Release, Pakete & Container (Stable)
|
||||||
│ └── testing.yaml # CI/CD-Workflow für Pre-Releases (testing-Branch)
|
│ └── testing.yaml # CI/CD: Preview, Pakete & Container (Testing)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer
|
│ ├── get-build-number.py # Dynamische Ermittlung der Build-/Revisionsnummer
|
||||||
│ └── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen
|
│ └── package-arch.py # Erstellung nativer Arch Linux-Pakete
|
||||||
├── src/
|
├── src/
|
||||||
│ └── main.rs # Quellcode & Einstiegspunkt der Anwendung
|
│ ├── main.rs # CLI-Einstiegspunkt & Befehlsausführung
|
||||||
├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch)
|
│ ├── lib.rs # Bibliotheks-Wurzel & Modulexporte
|
||||||
├── LICENSE # Lizenzdatei (Standard: GPL-3.0-or-later)
|
│ ├── cli.rs # Clap-CLI-Argumente & Optionen
|
||||||
├── AGENTS.md # Richtlinien und Leitfaden für KI-Coding-Agenten
|
│ ├── 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
|
└── README.md # Projektdokumentation
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Checkliste zur Verwendung als Template
|
## Lokale Entwicklung & Bauen
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
|
- **Rust & Cargo** (Stable Toolchain, Edition 2024 unterstützt)
|
||||||
|
- **Python 3** (für Paketierungsskripte)
|
||||||
|
|
||||||
- **Rust & Cargo** (aktuelle Stable-Version, Edition 2024 unterstützt)
|
### Befehle zum Bauen & Testen
|
||||||
- **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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Projekt bauen
|
# Syntax- und Typprüfung ausführen
|
||||||
cargo build
|
cargo check
|
||||||
|
|
||||||
# Tests ausführen
|
# Unit-Tests ausführen
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# Release-Build erstellen
|
# Release-Binary kompilieren
|
||||||
cargo build --release
|
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<VERSION>`) |
|
|
||||||
| `testing` | `.gitea/workflows/testing.yaml` | `testing` | Pre-Release (`v<VERSION>-preview`) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lizenz
|
## 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
+152
@@ -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<String>,
|
||||||
|
|
||||||
|
/// Gitea-API-Token mit Schreibberechtigung für Pakete
|
||||||
|
#[arg(long, env = "GITEA_TOKEN", global = true)]
|
||||||
|
pub gitea_token: Option<String>,
|
||||||
|
|
||||||
|
/// Gitea-Paket-Registry-Owner / Organisation
|
||||||
|
#[arg(long, env = "REGISTRY_OWNER", global = true)]
|
||||||
|
pub registry_owner: Option<String>,
|
||||||
|
|
||||||
|
/// Optionales GitHub-Token für API-Anfragen (vermeidet Rate-Limits)
|
||||||
|
#[arg(long, env = "GITHUB_TOKEN", global = true)]
|
||||||
|
pub github_token: Option<String>,
|
||||||
|
|
||||||
|
/// Benutzerdefinierter Pfad zur Konfigurationsdatei
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// 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<LogLevelArg> 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<String>,
|
||||||
|
|
||||||
|
/// 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<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ConfigAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
|
||||||
|
/// Gitea-API-Token
|
||||||
|
#[arg(long)]
|
||||||
|
gitea_token: Option<String>,
|
||||||
|
|
||||||
|
/// Gitea-Paket-Registry-Owner
|
||||||
|
#[arg(long)]
|
||||||
|
registry_owner: Option<String>,
|
||||||
|
|
||||||
|
/// GitHub Personal Access Token
|
||||||
|
#[arg(long)]
|
||||||
|
github_token: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
+125
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoConfig {
|
||||||
|
pub fn new(name: impl Into<String>, 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<String>,
|
||||||
|
/// Gitea-API-Authentifizierungstoken.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gitea_token: Option<String>,
|
||||||
|
/// Name des Gitea-Paket-Registry-Owners bzw. der Organisation.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub registry_owner: Option<String>,
|
||||||
|
/// Optionales GitHub Personal Access Token für höhere Rate-Limits.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub github_token: Option<String>,
|
||||||
|
/// Konfigurierte Repositories zur Spiegelung.
|
||||||
|
#[serde(default)]
|
||||||
|
pub repositories: Vec<RepoConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<AppConfig>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: F) -> Result<AppConfig, config_ctdra::ConfyError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut AppConfig),
|
||||||
|
{
|
||||||
|
config_ctdra::modify::<AppConfig, F>(f)
|
||||||
|
}
|
||||||
+148
@@ -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<String>, token: impl Into<String>, owner: impl Into<String>) -> 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<String> {
|
||||||
|
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<Self> {
|
||||||
|
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<UploadStatus> {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+259
@@ -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<Self> {
|
||||||
|
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<String>,
|
||||||
|
pub prerelease: bool,
|
||||||
|
pub draft: bool,
|
||||||
|
pub assets: Vec<ReleaseAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct GhApiRelease {
|
||||||
|
id: u64,
|
||||||
|
tag_name: String,
|
||||||
|
name: Option<String>,
|
||||||
|
draft: bool,
|
||||||
|
prerelease: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
assets: Vec<GhApiAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitHubClient {
|
||||||
|
/// Erstellt einen neuen GitHub-API-Client mit optionalem Authentifizierungstoken.
|
||||||
|
pub fn new(token: Option<String>) -> Result<Self> {
|
||||||
|
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<Vec<GitHubRelease>> {
|
||||||
|
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<GhApiRelease> = 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<ReleaseAsset> = 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<reqwest::Response> {
|
||||||
|
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]))
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
+194
-10
@@ -1,13 +1,197 @@
|
|||||||
fn main() {
|
use anyhow::Result;
|
||||||
// TODO: Hauptlogik der Anwendung implementieren
|
use clap::Parser;
|
||||||
println!("Hello, World!");
|
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)]
|
#[tokio::main]
|
||||||
mod tests {
|
async fn main() -> Result<()> {
|
||||||
#[test]
|
let cli = Cli::parse();
|
||||||
fn it_works() {
|
|
||||||
// TODO: Unit-Tests für die Anwendung schreiben
|
// Globalen Logging-Level initialisieren
|
||||||
assert_eq!(2 + 2, 4);
|
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 <owner/repo>' 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 <owner/repo>' 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(())
|
||||||
}
|
}
|
||||||
|
|||||||
+277
@@ -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<bool>,
|
||||||
|
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<String> = 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<PathBuf> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user