From f6f71f3c201ef7e8e4339cd31b80b8946e9edf5a Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Thu, 27 Aug 2026 20:45:53 +0200 Subject: [PATCH] Feat: Erstellt das Projekt --- .idea/rust-template.iml | 1 + AGENTS.md | 43 +- Cargo.lock | 2128 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 57 +- README.md | 221 ++-- src/cli.rs | 152 +++ src/config.rs | 125 +++ src/gitea.rs | 154 +++ src/github.rs | 259 +++++ src/lib.rs | 5 + src/main.rs | 204 +++- src/pipeline.rs | 277 +++++ tests/config_tests.rs | 53 + tests/gitea_tests.rs | 65 ++ tests/github_tests.rs | 70 ++ 15 files changed, 3647 insertions(+), 167 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/config.rs create mode 100644 src/gitea.rs create mode 100644 src/github.rs create mode 100644 src/lib.rs create mode 100644 src/pipeline.rs create mode 100644 tests/config_tests.rs create mode 100644 tests/gitea_tests.rs create mode 100644 tests/github_tests.rs diff --git a/.idea/rust-template.iml b/.idea/rust-template.iml index cf84ae4..bbe0a70 100644 --- a/.idea/rust-template.iml +++ b/.idea/rust-template.iml @@ -3,6 +3,7 @@ + diff --git a/AGENTS.md b/AGENTS.md index a90b81a..3cc456d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,26 @@ # AGENTS.md -Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Coding-Agenten (sowie Entwickler), die an diesem Repository oder daraus abgeleiteten Projekten arbeiten. +Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Coding-Agenten (sowie Entwickler), die an `mirror-package` arbeiten. --- ## 1. Projektübersicht & Philosophie -Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf: -- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`). -- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`). -- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions). +`mirror-package` ist eine Rust-basierte CLI-Anwendung zur automatisierten Synchronisation und Spiegelung von Linux-Paketen aus GitHub-Releases in selbstgehostete Gitea/Forgejo-Paket-Registries. + +### Kernfunktionen & Routing +- **Unterstützte Paketformate**: Debian (`.deb`), Fedora/RHEL (`.rpm`), Arch Linux (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`). +- **Distributions-Routing**: + - Stabile Releases: `.deb` wird in `pool/stable/main` **und** `pool/testing/main` geladen; `.rpm` nach `rpm/stable`; Arch nach `arch/stable`. + - Pre-Releases: `.deb` wird ausschließlich nach `pool/testing/main` geladen; `.rpm` nach `rpm/testing`; Arch nach `arch/testing`. +- **Konfigurationsverwaltung**: Typensichere Speicherung über `config-ctdra`. +- **Authentifizierung**: Tokenloser Zugriff auf öffentliche GitHub-Releases (optionaler Token konfigurierbar); Gitea-Token/URL/Owner via CLI, Env-Vars oder Config. ### Standards & Tech-Stack - **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`). - **Rust Toolchain**: Stable. - **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`). -- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert). +- **Lizenz**: GPL-3.0-or-later. --- @@ -32,10 +37,20 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C │ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer │ └── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete ├── src/ -│ └── main.rs # Einstiegspunkt der Anwendung +│ ├── main.rs # Einstiegspunkt der Anwendung & Befehlsausführung +│ ├── lib.rs # Bibliotheks-Wurzel & Modulexporte +│ ├── cli.rs # Clap CLI-Parser & Subcommands +│ ├── config.rs # Konfigurationsmodelle & config-ctdra Anbindung +│ ├── gitea.rs # Gitea Package API Client & Routing-Logik +│ ├── github.rs # GitHub API Client & Paket-Klassifizierung +│ └── pipeline.rs # Synchronisations-Pipeline & Tempfile-Management +├── tests/ +│ ├── config_tests.rs # Tests für Konfigurationsverwaltung & Pfade +│ ├── gitea_tests.rs # Tests für Gitea Routing & Endpunkte +│ └── github_tests.rs # Tests für Paket-Klassifizierung & Repo-Parsing ├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch ├── LICENSE # Lizenztext -├── README.md # Benutzerdokumentation & Setup-Checkliste +├── README.md # Benutzerdokumentation └── AGENTS.md # Dieses Agenten-Handbuch ``` @@ -45,9 +60,9 @@ Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und C ### 3.1 Code-Stil & Best Practices - **Idiomatisches Rust**: Nutze moderne Sprachfeatures der Rust Edition 2024. -- **Fehlerbehandlung**: Verwende aussagekräftige Fehlertypen (z. B. mit `thiserror` oder `anyhow` für CLIs). Vermeide unnötiges `unwrap()` oder `panic!()` im Produktivcode. -- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen. Behalte die bestehende Sprachkonvention bei. -- **Template-TODOs**: Wenn neue Vorlagen-Features oder Platzhalter ergänzt werden, markiere anpassungsbedürftige Stellen eindeutig mit `// TODO:` (Rust), `# TODO:` (TOML/Python/YAML). +- **Fehlerbehandlung**: Verwende aussagekräftige Fehlertypen (`anyhow` für CLI und Pipeline-Fehler). Vermeide `unwrap()` oder `panic!()` im Produktivcode. +- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen. +- **Logging**: Nutze `logger-ctdra` für strukturierte Konsolen- und Datei-Logs. ### 3.2 Paketierungs-Metadaten in `Cargo.toml` Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die drei Metadaten-Blöcke in `Cargo.toml` synchron gehalten werden: @@ -59,15 +74,13 @@ Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die - `pkgrel`, `arch`, `depends`, `optdepends`. ### 3.3 Skripte in `scripts/` -- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` (via `cargo metadata` oder Dateiparsing) oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden. -- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek: `json`, `subprocess`, `urllib`, `argparse`, `os`, `re`, `tempfile`, `tarfile` etc.). +- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden. +- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek). --- ## 4. Workflows: Bauen, Testen & Validieren -Agenten müssen Änderungen vor dem Abschluss validieren. - ### 4.1 Grundlegende Validierung ```bash # Syntax- und Typprüfung diff --git a/Cargo.lock b/Cargo.lock index ad94a61..8264e99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,2129 @@ version = 4 [[package]] -name = "rust-template" -version = "0.0.0" +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc 0.2.189", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "config-ctdra" +version = "1.0.4" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "f7b3e25b95d4cdb6fc856f2af0106d4558b882c2f10798db46181e51827749a4" +dependencies = [ + "confy", + "program-ctdra", + "serde", + "sudo-ctdra", +] + +[[package]] +name = "confy" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8807c397789cbe02bbdb1a27ea5f345584132808697b2a3f957c829829ee4814" +dependencies = [ + "etcetera", + "lazy_static", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc 0.2.189", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc 0.2.189", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc 0.2.189", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc 0.2.189", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.59.0", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc 0.2.189", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc 0.2.189", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc 0.2.189", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc 0.2.189", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libc" +version = "1.0.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "logger-ctdra" +version = "1.0.3" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "af9ea239d80163c80b12cae5f74c0c2824199921dedafba74678e96a11a8b15e" +dependencies = [ + "program-ctdra", + "time", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc 0.2.189", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mirror-package" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "config-ctdra", + "futures-util", + "logger-ctdra", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc 0.2.189", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc 0.2.189", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "program-ctdra" +version = "1.0.0" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "54f20af67f90bf6d10697dd77bb4beb41dafad7fe0fb46aacfc81ad45a8e48bd" + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc 0.2.189", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc 0.2.189", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc 0.2.189", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc 0.2.189", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc 0.2.189", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc 0.2.189", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sudo-ctdra" +version = "1.0.0" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "2c2746122bbe36f821de70db3ca4ad24edfb77118fc6a6bc7693082b50d23e5f" +dependencies = [ + "libc 1.0.0-alpha.4", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc 0.2.189", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc 0.2.189", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc 0.2.189", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc 0.2.189", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 0cb0754..95a7d17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,23 @@ [package] -name = "rust-template" # TODO: Projektnamen setzen -version = "0.0.0" # TODO: Initiale Version setzen (z. B. 0.1.0) +name = "mirror-package" +version = "0.1.0" edition = "2024" -authors = ['DragonSlayer_14'] # TODO: Autor(en) anpassen +authors = ['DragonSlayer_14'] readme = "README.md" -license = "GPL-3.0-or-later" # TODO: Lizenz prüfen / anpassen -repository = "https://gitea.creative-dragonslayer.de/Linuxapps/rust-template" # TODO: Repository-URL anpassen -description = "" # TODO: Kurze Projektbeschreibung setzen +license = "GPL-3.0-or-later" +repository = "https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage" +description = "Mirror prebuilt Linux packages from GitHub Releases to Gitea Package Registry" [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" +futures-util = "0.3" [profile.release] debug = "none" @@ -17,28 +25,23 @@ debug = "none" # --- Paketierungs-Metadaten für Linux-Distributionen --- [package.metadata.deb] -name = "" # TODO: Optional (Standard: package.name) -maintainer = "" # TODO: Maintainer setzen (z. B. "Max Mustermann ") -copyright = "" # TODO: Copyright-Hinweis setzen (z. B. "2026 Max Mustermann") -section = "utils" # TODO: Debian-Sektion anpassen (z. B. utils, net, admin) -priority = "optional" # TODO: Debian-Priorität anpassen (z. B. optional) -depends = "$auto" # TODO: Abhängigkeiten definieren ($auto ermittelt via ldd automatisch) +name = "mirror-package" +maintainer = "DragonSlayer_14" +copyright = "2026 DragonSlayer_14" +section = "utils" +priority = "optional" +depends = "$auto" extended-description = """\ -""" # TODO: Ausführliche Paketbeschreibung setzen -assets = [ - # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.: - # ["target/release/rust-template", "usr/bin/", "755"], -] +Mirror prebuilt Linux packages (.deb, .rpm, .pkg.tar.zst) from GitHub Releases into Gitea Package Registries.\ +""" +assets = [] [package.metadata.generate-rpm] -assets = [ - # TODO: Bei Bedarf zusätzliche Dateien / Assets definieren, z. B.: - # { source = "target/release/rust-template", dest = "/usr/bin/rust-template", mode = "755" }, -] -requires = { } # TODO: RPM-Abhängigkeiten definieren, z. B. { "glibc" = "*" } +assets = [] +requires = { } [package.metadata.arch] -pkgrel = "1" # TODO: Standard Release-Nummer (wird in CI durch get-build-number.py dynamisch überschrieben) -arch = "x86_64" # TODO: Zielarchitektur (z. B. x86_64, aarch64, i686) -depends = ["gcc-libs", "glibc"] # TODO: Arch Linux Paketabhängigkeiten definieren -optdepends = [] # TODO: Optionale Abhängigkeiten definieren (z. B. ["curl: für Web-Downloads"]) +pkgrel = "1" +arch = "x86_64" +depends = ["gcc-libs", "glibc"] +optdepends = [] diff --git a/README.md b/README.md index 56e1fe8..07f8917 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,143 @@ -# rust-template +# mirror-package -Ein modernes Template-Projekt für Rust-basierte Linux-Anwendungen und Kommandozeilen-Tools (CLI). +Automated tool to extract, download, and mirror prebuilt Linux packages from GitHub Releases into a self-hosted Gitea / Forgejo Package 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` tracks configured GitHub repositories (such as `raspberrypi/rpi-imager` or `Heroic-Games-Launcher/HeroicGamesLauncher`), identifies precompiled Linux packages (`.deb`, `.rpm`, `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`), and automatically publishes them into the appropriate Gitea Package Registry distributions according to release stability rules. --- ## Features -- **Rust Edition 2024**: Moderner Rust-Sprachstandard. -- **Multi-Architektur-Kompilierung**: - - `x86_64-unknown-linux-gnu` (64-Bit x86) - - `aarch64-unknown-linux-gnu` (64-Bit ARM / ARM64) - - `i686-unknown-linux-gnu` (32-Bit x86) -- **Linux-Paketierung out-of-the-box**: - - **Debian / Ubuntu** (`.deb` via `cargo-deb`) - - **Fedora / RHEL / openSUSE** (`.rpm` via `cargo-generate-rpm`) - - **Arch Linux** (`.pkg.tar.zst` via mitgeliefertem `scripts/package-arch.py`) -- **Automatisierte CI/CD-Pipelines**: - - `main`-Branch: Erstellt stabile Builds, ermittelt dynamisch Build-Nummern, paketiert für alle Architekturen, lädt Pakete in die Gitea Package Registry und erstellt Gitea Releases mit Dateianhängen. - - `testing`-Branch: Erstellt Preview-Builds und Pre-Releases mit Testing-Paketen. -- **Automatisierte Versions- & Build-Nummern**: - - `scripts/get-build-number.py` fragt Gitea Releases, die Package Registry sowie lokale Artefakte ab, um Revisions-/Release-Nummern (z. B. `1.0.0-1`, `1.0.0-2`) automatisch zu erhöhen. -- **Vorkonfigurierte Cargo-Einstellungen**: - - `.cargo/config.toml` mit vorkonfigurierten Cross-Compilation-Linkern und Unterstützung für private/öffentliche Cargo Registries. +- **Multi-Distribution Package Support**: + - **Debian / Ubuntu** (`.deb`) + - **Fedora / RHEL / openSUSE** (`.rpm`) + - **Arch Linux** (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`) +- **Distribution Routing Rules**: + - **Stable Releases**: Debian packages are published to **both** `stable` and `testing` Debian pools (`pool/stable/main` and `pool/testing/main`); RPM packages to `rpm/stable`; Arch packages to `arch/stable`. + - **Pre-Releases**: Published **only** to testing channels: Debian `pool/testing/main`, RPM `rpm/testing`, and Arch `arch/testing`. +- **Persistent Configuration with `config-ctdra`**: + - Automatically stores settings in user (`~/.config/mirror-package/config.toml`) or system (`/etc/mirror-package/config.toml`) paths. + - Adding or removing repositories via CLI automatically updates the configuration file. +- **Flexible Authentication**: + - GitHub releases can be queried anonymously (no token required). + - Optional GitHub Personal Access Token support for higher API rate limits. + - Gitea instance URL, API token, and registry owner configurable via CLI flags, environment variables, or config file. +- **Diagnostics & Dry-Run Mode**: + - Unified file and console logging powered by `logger-ctdra`. + - `--dry-run` flag to inspect download and upload steps without making remote changes. --- -## Struktur des Repositories +## CLI Usage & Commands + +### 1. Configuration (`config`) +Set up your Gitea credentials and optional GitHub token: + +```bash +# Configure Gitea instance and registry owner +mirror-package config set \ + --gitea-url "https://gitea.creative-dragonslayer.de" \ + --gitea-token "your_gitea_api_token" \ + --registry-owner "Linuxapps" + +# Optional: Configure GitHub Token for rate limits +mirror-package config set --github-token "ghp_xxxxxxxxxxxx" + +# Display current configuration +mirror-package config show +``` + +Credentials can also be passed via environment variables (`GITEA_URL`, `GITEA_TOKEN`, `REGISTRY_OWNER`, `GITHUB_TOKEN`) or global CLI arguments. + +### 2. Managing Monitored Repositories (`add`, `remove`, `list`) + +```bash +# Add repositories to persistent configuration +mirror-package add raspberrypi/rpi-imager +mirror-package add Heroic-Games-Launcher/HeroicGamesLauncher + +# Add repository without pre-release syncing +mirror-package add some-owner/some-repo --no-prereleases + +# List configured repositories and their sync status +mirror-package list + +# Remove a repository +mirror-package remove raspberrypi/rpi-imager +``` + +### 3. Synchronizing Packages (`sync`) + +```bash +# Sync all configured repositories (latest stable and latest pre-release) +mirror-package sync + +# Sync a specific repository +mirror-package sync raspberrypi/rpi-imager + +# Sync all historical releases for a repository +mirror-package sync raspberrypi/rpi-imager --history + +# Dry-run test (simulates without uploading) +mirror-package sync --dry-run +``` + +--- + +## Repository Structure ```text ├── .cargo/ -│ └── config.toml # Cargo-Konfiguration (Linker für Cross-Compiling, Registry) +│ └── config.toml # Linker & Cargo configuration ├── .gitea/ │ └── workflows/ -│ ├── main.yaml # CI/CD-Workflow für stabile Releases (main-Branch) -│ └── testing.yaml # CI/CD-Workflow für Pre-Releases (testing-Branch) +│ ├── main.yaml # CI/CD: Release & Multi-Arch Package Build +│ └── testing.yaml # CI/CD: Preview & Testing Build ├── scripts/ -│ ├── get-build-number.py # Dynamische Ermittlung der nächsten Paket-Revisionsnummer -│ └── package-arch.py # Erstellung von Arch Linux .pkg.tar.zst Paketen +│ ├── get-build-number.py # Dynamic build/revision number resolution +│ └── package-arch.py # Native Arch Linux package builder ├── src/ -│ └── main.rs # Quellcode & Einstiegspunkt der Anwendung -├── Cargo.toml # Cargo Manifest & Paketierungsmetadaten (deb, rpm, arch) -├── LICENSE # Lizenzdatei (Standard: GPL-3.0-or-later) -├── AGENTS.md # Richtlinien und Leitfaden für KI-Coding-Agenten -└── README.md # Projektdokumentation +│ ├── main.rs # CLI entrypoint & subcommand dispatch +│ ├── lib.rs # Library root & module exports +│ ├── cli.rs # Clap CLI arguments & options +│ ├── config.rs # config-ctdra integration & models +│ ├── gitea.rs # Gitea Package Registry upload client +│ ├── github.rs # GitHub API client & package classifier +│ └── pipeline.rs # End-to-end sync workflow & temp storage +├── tests/ +│ ├── config_tests.rs # Tests for configuration management +│ ├── gitea_tests.rs # Tests for Gitea upload routing +│ └── github_tests.rs # Tests for package classification & parsing +├── Cargo.toml # Project manifest and packaging metadata +├── LICENSE # GPL-3.0-or-later License +├── AGENTS.md # Agent & developer guidelines +└── README.md # Documentation ``` --- -## Checkliste zur Verwendung als Template +## Local Development & Building -Wenn du ein neues Projekt aus diesem Template erstellst, gehe folgende Schritte durch: +### Prerequisites +- **Rust & Cargo** (Stable toolchain, Edition 2024 supported) +- **Python 3** (for packaging scripts) -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 - -- **Rust & Cargo** (aktuelle Stable-Version, Edition 2024 unterstützt) -- **Python 3** (für Hilfsskripte in `scripts/`) -- Für Cross-Compilation (optional): - - `rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu i686-unknown-linux-gnu` - - Cross-Toolchains: `gcc-aarch64-linux-gnu`, `gcc-i686-linux-gnu` -- Für lokales Paketieren (optional): - - `cargo install cargo-deb cargo-generate-rpm` - -### Wichtige Befehle +### Build & Test Commands ```bash -# Projekt bauen -cargo build +# Run syntax and type checks +cargo check -# Tests ausführen +# Run unit tests cargo test -# Release-Build erstellen +# Build release binary 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 +## License -### `scripts/get-build-number.py` -Ermittelt dynamisch die nächste Build-/Paketnummer (pkgrel / revision). -- Liest Name und Version automatisch aus `Cargo.toml`. -- Fragt Gitea Releases und Package Registry ab, falls Token/URL vorhanden. -- Berücksichtigt lokal im Verzeichnis `target/` liegende Pakete. -- Aufruf: `python3 scripts/get-build-number.py` - -### `scripts/package-arch.py` -Erstellt native Arch Linux-Pakete (`.pkg.tar.zst`), ohne dass `makepkg` oder eine Arch Linux-Installation erforderlich ist. -- Liest Konfiguration aus `[package.metadata.arch]` in `Cargo.toml`. -- Installiert die Binary nach `/usr/bin/`, sowie `LICENSE` und `README.md` nach `/usr/share/`. -- Erzeugt eine standardkonforme `.PKGINFO`-Datei und komprimiert das Paket mit `zstandard`. -- Parameter: - - `--target`: Rust Target-Triple (z. B. `x86_64-unknown-linux-gnu`) - - `--arch`: Zielarchitektur (z. B. `x86_64`, `aarch64`, `i686`) - - `--pkgrel`: Release-Nummer - ---- - -## CI/CD Workflow-Übersicht - -| Branch | Workflow | Paket-Kanal | Release-Typ | -| :--- | :--- | :--- | :--- | -| `main` | `.gitea/workflows/main.yaml` | `stable` | Offizielles Release (`v`) | -| `testing` | `.gitea/workflows/testing.yaml` | `testing` | Pre-Release (`v-preview`) | - ---- - -## Lizenz - -Dieses Template steht standardmäßig unter der [GPL-3.0-or-later](LICENSE)-Lizenz. Die Lizenz kann bei Bedarf in `LICENSE` und `Cargo.toml` angepasst werden. +This project is licensed under the [GPL-3.0-or-later](LICENSE) license. diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..d4929c7 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,152 @@ +use clap::{Args, Parser, Subcommand, ValueEnum}; +use std::path::PathBuf; + +/// Command line parser for mirror-package. +#[derive(Parser, Debug)] +#[command( + name = "mirror-package", + author, + version, + about = "Mirror Linux packages (.deb, .rpm, .pkg.tar.zst) from GitHub Releases to Gitea Package Registry", + long_about = None +)] +pub struct Cli { + /// Gitea instance base URL (e.g. https://gitea.creative-dragonslayer.de) + #[arg(long, env = "GITEA_URL", global = true)] + pub gitea_url: Option, + + /// Gitea API Token with package write permissions + #[arg(long, env = "GITEA_TOKEN", global = true)] + pub gitea_token: Option, + + /// Gitea Package Registry owner / organization + #[arg(long, env = "REGISTRY_OWNER", global = true)] + pub registry_owner: Option, + + /// Optional GitHub Token for API requests (avoids rate limits) + #[arg(long, env = "GITHUB_TOKEN", global = true)] + pub github_token: Option, + + /// Custom path to configuration file + #[arg(long, global = true)] + pub config: Option, + + /// Perform a dry run without downloading or uploading packages + #[arg(long, global = true)] + pub dry_run: bool, + + /// Logging level (error, warn, info, debug) + #[arg(long, value_enum, default_value_t = LogLevelArg::Info, global = true)] + pub log_level: LogLevelArg, + + #[command(subcommand)] + pub command: Commands, +} + +#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] +pub enum LogLevelArg { + Error, + Warn, + Info, + Debug, +} + +impl From for logger_ctdra::LogLevel { + fn from(arg: LogLevelArg) -> Self { + match arg { + LogLevelArg::Error => logger_ctdra::LogLevel::Error, + LogLevelArg::Warn => logger_ctdra::LogLevel::Warn, + LogLevelArg::Info => logger_ctdra::LogLevel::Info, + LogLevelArg::Debug => logger_ctdra::LogLevel::Debug, + } + } +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Synchronize packages from GitHub Releases to Gitea Package Registry + Sync(SyncArgs), + + /// Add a GitHub repository to the persistent configuration + Add(AddArgs), + + /// Remove a GitHub repository from the persistent configuration + #[command(alias = "rm")] + Remove(RemoveArgs), + + /// List configured repositories and their sync status + #[command(alias = "ls")] + List, + + /// View or update application configuration + Config(ConfigArgs), +} + +#[derive(Args, Debug)] +pub struct SyncArgs { + /// Specific repository name or URL to sync (e.g. "raspberrypi/rpi-imager"). If omitted, all configured repositories are synced. + pub repo: Option, + + /// Sync all configured repositories + #[arg(short = 'a', long)] + pub all: bool, + + /// Scan historical releases instead of only the latest release(s) + #[arg(long)] + pub history: bool, + + /// Include pre-releases during this sync run (defaults to repository configuration if not specified) + #[arg(long)] + pub prereleases: Option, +} + +#[derive(Args, Debug)] +pub struct AddArgs { + /// GitHub repository to mirror (e.g. "raspberrypi/rpi-imager" or full GitHub URL) + pub repo: String, + + /// Do not mirror pre-releases for this repository + #[arg(long)] + pub no_prereleases: bool, + + /// Immediately synchronize this repository after adding + #[arg(long)] + pub sync: bool, +} + +#[derive(Args, Debug)] +pub struct RemoveArgs { + /// Repository to remove (e.g. "raspberrypi/rpi-imager") + pub repo: String, +} + +#[derive(Args, Debug)] +pub struct ConfigArgs { + #[command(subcommand)] + pub action: Option, +} + +#[derive(Subcommand, Debug)] +pub enum ConfigAction { + /// Show current configuration and configuration file location + Show, + + /// Set configuration parameters + Set { + /// Gitea instance base URL + #[arg(long)] + gitea_url: Option, + + /// Gitea API Token + #[arg(long)] + gitea_token: Option, + + /// Gitea Package Registry owner + #[arg(long)] + registry_owner: Option, + + /// GitHub Personal Access Token + #[arg(long)] + github_token: Option, + }, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cec51a8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,125 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +fn default_true() -> bool { + true +} + +/// Configuration for an individual repository to mirror. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct RepoConfig { + /// Full name of the repository in "owner/repo" format. + pub name: String, + /// Whether to mirror pre-releases. + #[serde(default = "default_true")] + pub include_prereleases: bool, + /// Last successfully synced tag name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_synced_tag: Option, +} + +impl RepoConfig { + pub fn new(name: impl Into, include_prereleases: bool) -> Self { + Self { + name: name.into(), + include_prereleases, + last_synced_tag: None, + } + } +} + +/// Persistent application configuration. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct AppConfig { + /// Gitea instance base URL (e.g. https://gitea.example.com). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gitea_url: Option, + /// Gitea API authentication token. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gitea_token: Option, + /// Gitea Package Registry owner/organization name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub registry_owner: Option, + /// Optional GitHub Personal Access Token for increased rate limits. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub github_token: Option, + /// Configured repositories to mirror. + #[serde(default)] + pub repositories: Vec, +} + +impl AppConfig { + /// Normalizes repository input, stripping leading/trailing slashes and GitHub URL prefixes. + 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() + } + + /// Adds or updates a repository in the configuration. + 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); + } + } + + /// Removes a repository by name. Returns true if it was found and removed. + 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 + } + + /// Finds a repository by name. + 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)) + } + + /// Updates the last synced tag for a given 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); + } + } +} + +/// Sets a custom path for the configuration file if provided. +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()); + } +} + +/// Returns the resolved path of the configuration file. +pub fn get_config_file_path() -> PathBuf { + config_ctdra::get_config_path() +} + +/// Loads the application configuration. Falls back to defaults if not found. +pub fn load_config() -> AppConfig { + config_ctdra::load_config::() +} + +/// Saves the application configuration to disk. +pub fn save_config(config: &AppConfig) -> Result<(), config_ctdra::ConfyError> { + config_ctdra::store(config) +} + +/// Modifies the application configuration in-place atomically and saves it. +pub fn modify_config(f: F) -> Result +where + F: FnOnce(&mut AppConfig), +{ + config_ctdra::modify::(f) +} diff --git a/src/gitea.rs b/src/gitea.rs new file mode 100644 index 0000000..e51b45a --- /dev/null +++ b/src/gitea.rs @@ -0,0 +1,154 @@ +use crate::github::PackageType; +use anyhow::{bail, Context, Result}; +use std::path::Path; +use tokio::fs::File; + +/// Credentials and target registry information for Gitea. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GiteaConfig { + pub base_url: String, + pub token: String, + pub owner: String, +} + +impl GiteaConfig { + pub fn new(base_url: impl Into, token: impl Into, owner: impl Into) -> Self { + let mut base_url = base_url.into(); + while base_url.ends_with('/') { + base_url.pop(); + } + Self { + base_url, + token: token.into(), + owner: owner.into(), + } + } +} + +/// Result of a package upload attempt. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UploadStatus { + Uploaded, + AlreadyExists, + SimulatedDryRun, +} + +/// Computes the target Gitea package registry upload URLs based on package type and release channel. +pub fn get_target_upload_urls( + base_url: &str, + owner: &str, + pkg_type: PackageType, + prerelease: bool, +) -> Vec { + let base = base_url.trim_end_matches('/'); + match pkg_type { + PackageType::Debian => { + if prerelease { + vec![format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner)] + } else { + vec![ + format!("{}/api/packages/{}/debian/pool/stable/main/upload", base, owner), + format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner), + ] + } + } + PackageType::Rpm => { + if prerelease { + vec![format!("{}/api/packages/{}/rpm/testing/upload", base, owner)] + } else { + vec![ + format!("{}/api/packages/{}/rpm/stable/upload", base, owner), + format!("{}/api/packages/{}/rpm/testing/upload", base, owner), + ] + } + } + PackageType::Arch => { + if prerelease { + vec![format!("{}/api/packages/{}/arch/testing", base, owner)] + } else { + vec![ + format!("{}/api/packages/{}/arch/stable", base, owner), + format!("{}/api/packages/{}/arch/testing", base, owner), + ] + } + } + } +} + +/// Client for uploading packages to a Gitea Package Registry. +#[derive(Clone, Debug)] +pub struct GiteaClient { + client: reqwest::Client, + config: GiteaConfig, +} + +impl GiteaClient { + /// Creates a new Gitea client. + pub fn new(config: GiteaConfig) -> Result { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::USER_AGENT, + reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .context("Failed to initialize Gitea HTTP client")?; + + Ok(Self { client, config }) + } + + /// Uploads a package file from disk to the specified Gitea endpoint. + pub async fn upload_file( + &self, + file_path: &Path, + endpoint_url: &str, + dry_run: bool, + ) -> Result { + if dry_run { + return Ok(UploadStatus::SimulatedDryRun); + } + + let file = File::open(file_path) + .await + .with_context(|| format!("Failed to open package file {:?}", file_path))?; + + let body = reqwest::Body::from(file); + + let mut auth_header_val = format!("token {}", self.config.token); + if self.config.token.starts_with("Bearer ") || self.config.token.starts_with("token ") { + auth_header_val = self.config.token.clone(); + } + + let resp = self + .client + .put(endpoint_url) + .header(reqwest::header::AUTHORIZATION, auth_header_val) + .body(body) + .send() + .await + .with_context(|| format!("Failed to send PUT request to {}", endpoint_url))?; + + let status = resp.status(); + if status.is_success() { + return Ok(UploadStatus::Uploaded); + } + + let error_body = resp.text().await.unwrap_or_default(); + if status == reqwest::StatusCode::CONFLICT + || status == reqwest::StatusCode::UNPROCESSABLE_ENTITY + || error_body.to_ascii_lowercase().contains("already exists") + || error_body.to_ascii_lowercase().contains("duplicate") + { + return Ok(UploadStatus::AlreadyExists); + } + + bail!( + "Failed to upload package to {} (HTTP {}): {}", + endpoint_url, + status, + error_body + ); + } +} diff --git a/src/github.rs b/src/github.rs new file mode 100644 index 0000000..7f49fa1 --- /dev/null +++ b/src/github.rs @@ -0,0 +1,259 @@ +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; + +/// Supported Linux package distribution types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum PackageType { + Debian, + Rpm, + Arch, +} + +impl PackageType { + /// Classifies an asset by its filename into a supported PackageType. + pub fn from_filename(filename: &str) -> Option { + let name = filename.to_ascii_lowercase(); + // Check Arch packages first due to multi-part extensions + 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 + } + } + + /// Returns human-readable name of the package type. + pub fn name(&self) -> &'static str { + match self { + PackageType::Debian => "Debian (.deb)", + PackageType::Rpm => "Fedora/RPM (.rpm)", + PackageType::Arch => "Arch Linux (.pkg.tar.zst)", + } + } +} + +/// A classified Linux package asset from a release. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ReleaseAsset { + pub name: String, + pub download_url: String, + pub size: u64, + pub package_type: PackageType, +} + +/// A GitHub release with extracted Linux package assets. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GitHubRelease { + pub id: u64, + pub tag_name: String, + pub name: Option, + pub prerelease: bool, + pub draft: bool, + pub assets: Vec, +} + +#[derive(Deserialize, Debug)] +struct GhApiRelease { + id: u64, + tag_name: String, + name: Option, + draft: bool, + prerelease: bool, + #[serde(default)] + assets: Vec, +} + +#[derive(Deserialize, Debug)] +struct GhApiAsset { + name: String, + browser_download_url: String, + size: u64, +} + +/// Client for interacting with the GitHub API. +#[derive(Clone, Debug)] +pub struct GitHubClient { + client: reqwest::Client, + token: Option, +} + +impl GitHubClient { + /// Creates a new GitHub API client with optional authentication token. + pub fn new(token: Option) -> Result { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::USER_AGENT, + reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))), + ); + headers.insert( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/vnd.github+json"), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .context("Failed to initialize HTTP client")?; + + Ok(Self { client, token }) + } + + /// Fetches releases for a given repository (format "owner/repo"). + /// + /// If `history` is false, retrieves only the latest release(s). + /// If `include_prereleases` is false, skips pre-releases. + pub async fn fetch_releases( + &self, + repo: &str, + history: bool, + include_prereleases: bool, + ) -> Result> { + let (owner, name) = parse_repo_owner_name(repo)?; + + let mut releases = Vec::new(); + let page_size = 30; + let mut page = 1; + + loop { + let url = format!( + "https://api.github.com/repos/{}/{}/releases?per_page={}&page={}", + owner, name, page_size, page + ); + + let mut req = self.client.get(&url); + if let Some(token) = &self.token { + req = req.bearer_auth(token); + } + + let resp = req.send().await.with_context(|| format!("Failed to send request to {}", url))?; + let status = resp.status(); + + if !status.is_success() { + let error_text = resp.text().await.unwrap_or_default(); + bail!( + "GitHub API request failed for {}/{} (HTTP {}): {}", + owner, + name, + status, + error_text + ); + } + + let raw_releases: Vec = resp + .json() + .await + .with_context(|| format!("Failed to parse GitHub releases JSON for {}/{}", owner, name))?; + + if raw_releases.is_empty() { + break; + } + + for raw in raw_releases { + if raw.draft { + continue; + } + if raw.prerelease && !include_prereleases { + continue; + } + + let filtered_assets: Vec = raw + .assets + .into_iter() + .filter_map(|a| { + PackageType::from_filename(&a.name).map(|pkg_type| ReleaseAsset { + name: a.name, + download_url: a.browser_download_url, + size: a.size, + package_type: pkg_type, + }) + }) + .collect(); + + releases.push(GitHubRelease { + id: raw.id, + tag_name: raw.tag_name, + name: raw.name, + prerelease: raw.prerelease, + draft: raw.draft, + assets: filtered_assets, + }); + } + + // If not scanning full history, stop after the first page (or once we found stable/prerelease) + if !history { + // Keep only the most recent stable release and most recent pre-release (if allowed) + 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 { + // Avoid duplicates if stable and pre-release are identical (rare) + if !selected.iter().any(|r| r.id == prerelease.id) { + selected.push(prerelease.clone()); + } + } + } + return Ok(selected); + } + + page += 1; + } + + Ok(releases) + } + + /// Downloads an asset from the given download URL as a streaming response. + pub async fn download_asset_stream(&self, download_url: &str) -> Result { + let mut req = self.client.get(download_url); + if let Some(token) = &self.token { + req = req.bearer_auth(token); + } + + let resp = req + .send() + .await + .with_context(|| format!("Failed to start download from {}", download_url))?; + + if !resp.status().is_success() { + bail!( + "Download failed for {} (HTTP status {})", + download_url, + resp.status() + ); + } + + Ok(resp) + } +} + +/// Helper to parse "owner/repo" from repository string. +pub fn parse_repo_owner_name(repo: &str) -> Result<(&str, &str)> { + let cleaned = repo + .trim() + .trim_start_matches("https://github.com/") + .trim_start_matches("http://github.com/") + .trim_start_matches("github.com/") + .trim_end_matches(".git") + .trim_matches('/'); + + let parts: Vec<&str> = cleaned.split('/').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + bail!( + "Invalid repository format '{}'. Expected 'owner/repo' (e.g. 'raspberrypi/rpi-imager').", + repo + ); + } + + Ok((parts[0], parts[1])) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d9bc105 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod cli; +pub mod config; +pub mod gitea; +pub mod github; +pub mod pipeline; diff --git a/src/main.rs b/src/main.rs index 79004d9..46c7aac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,197 @@ -fn main() { - // TODO: Hauptlogik der Anwendung implementieren - println!("Hello, World!"); -} +use anyhow::Result; +use clap::Parser; +use logger_ctdra::{log, set_log_level, LogLevel}; +use mirror_package::cli::{Cli, Commands, ConfigAction}; +use mirror_package::config::{ + get_config_file_path, init_config_path, load_config, modify_config, AppConfig, RepoConfig, +}; +use mirror_package::pipeline::{sync_single_repository, SyncOptions, SyncReport}; -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - // TODO: Unit-Tests für die Anwendung schreiben - assert_eq!(2 + 2, 4); +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + // Initialize global logger level + set_log_level(cli.log_level.into()); + + // Initialize custom config path if specified + init_config_path(cli.config.as_deref()); + + // Load persistent configuration + let mut app_config = load_config(); + + // CLI flags override configuration values + if let Some(url) = cli.gitea_url { + app_config.gitea_url = Some(url); } + if let Some(token) = cli.gitea_token { + app_config.gitea_token = Some(token); + } + if let Some(owner) = cli.registry_owner { + app_config.registry_owner = Some(owner); + } + if let Some(gh_token) = cli.github_token { + app_config.github_token = Some(gh_token); + } + + match cli.command { + Commands::Sync(sync_args) => { + let options = SyncOptions { + history: sync_args.history, + prereleases_override: sync_args.prereleases, + dry_run: cli.dry_run, + }; + + let repos_to_sync = if let Some(specific_repo) = sync_args.repo { + let norm = AppConfig::normalize_repo_name(&specific_repo); + let prereleases = sync_args.prereleases.unwrap_or( + app_config + .find_repo(&norm) + .map(|r| r.include_prereleases) + .unwrap_or(true), + ); + vec![RepoConfig::new(norm, prereleases)] + } else if !app_config.repositories.is_empty() { + app_config.repositories.clone() + } else { + log( + "sync", + "No repositories configured. Add one with 'mirror-package add ' or specify a repository to sync.", + LogLevel::Warn, + ); + return Ok(()); + }; + + let mut report = SyncReport::default(); + + for repo in repos_to_sync { + if let Err(e) = sync_single_repository(&repo, &app_config, &options, &mut report).await { + log( + "sync", + &format!("Failed to sync repository '{}': {:#}", repo.name, e), + LogLevel::Error, + ); + } + } + + log( + "sync", + &format!( + "Sync finished: {} repository(ies) processed, {} package(s) uploaded, {} skipped (already exist), {} simulated (dry-run).", + report.repositories_processed, + report.packages_uploaded, + report.packages_already_exist, + report.packages_simulated + ), + LogLevel::Info, + ); + } + + Commands::Add(add_args) => { + let normalized_name = AppConfig::normalize_repo_name(&add_args.repo); + let include_prereleases = !add_args.no_prereleases; + + let updated_repo = RepoConfig::new(&normalized_name, include_prereleases); + let to_add = updated_repo.clone(); + + modify_config(move |cfg| { + cfg.add_or_update_repo(to_add); + })?; + + log( + "config", + &format!( + "Repository '{}' added (include pre-releases: {}).", + normalized_name, include_prereleases + ), + LogLevel::Info, + ); + + if add_args.sync { + let options = SyncOptions { + history: false, + prereleases_override: None, + dry_run: cli.dry_run, + }; + let mut report = SyncReport::default(); + sync_single_repository(&updated_repo, &app_config, &options, &mut report).await?; + } + } + + Commands::Remove(rm_args) => { + let normalized_name = AppConfig::normalize_repo_name(&rm_args.repo); + let target_name = normalized_name.clone(); + + modify_config(move |cfg| { + cfg.remove_repo(&target_name); + })?; + + log( + "config", + &format!("Repository '{}' removed from configuration.", normalized_name), + LogLevel::Info, + ); + } + + Commands::List => { + println!("Configured repositories (Config file: {:?}):", get_config_file_path()); + if app_config.repositories.is_empty() { + println!(" (None configured yet. Use 'mirror-package add ' to add one.)"); + } else { + println!("{:<40} {:<15} {:<20}", "REPOSITORY", "PRE-RELEASES", "LAST SYNCED TAG"); + println!("{:-<40} {:-<15} {:-<20}", "", "", ""); + for repo in &app_config.repositories { + println!( + "{:<40} {:<15} {:<20}", + repo.name, + if repo.include_prereleases { "yes" } else { "no" }, + repo.last_synced_tag.as_deref().unwrap_or("-") + ); + } + } + } + + Commands::Config(config_args) => match config_args.action { + None | Some(ConfigAction::Show) => { + println!("Configuration location: {:?}", get_config_file_path()); + println!("Gitea URL: {}", app_config.gitea_url.as_deref().unwrap_or("(not configured)")); + println!("Registry Owner: {}", app_config.registry_owner.as_deref().unwrap_or("(not configured)")); + println!( + "Gitea Token: {}", + if app_config.gitea_token.is_some() { "******** (set)" } else { "(not configured)" } + ); + println!( + "GitHub Token: {}", + if app_config.github_token.is_some() { "******** (set)" } else { "(not configured, public access only)" } + ); + println!("Repositories: {} configured", app_config.repositories.len()); + } + + Some(ConfigAction::Set { + gitea_url, + gitea_token, + registry_owner, + github_token, + }) => { + modify_config(move |cfg| { + if let Some(url) = gitea_url { + cfg.gitea_url = Some(url); + } + if let Some(tok) = gitea_token { + cfg.gitea_token = Some(tok); + } + if let Some(own) = registry_owner { + cfg.registry_owner = Some(own); + } + if let Some(gh) = github_token { + cfg.github_token = Some(gh); + } + })?; + + log("config", "Configuration successfully updated.", LogLevel::Info); + } + }, + } + + Ok(()) } diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..8f5e4d9 --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,277 @@ +use crate::config::{modify_config, AppConfig, RepoConfig}; +use crate::gitea::{get_target_upload_urls, GiteaClient, GiteaConfig, UploadStatus}; +use crate::github::GitHubClient; +use anyhow::{bail, Context, Result}; +use logger_ctdra::{log, LogLevel}; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::fs::{remove_file, File}; +use tokio::io::AsyncWriteExt; + +/// Summary statistics of a synchronization operation. +#[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, +} + +/// Options controlling a repository sync run. +#[derive(Debug, Clone)] +pub struct SyncOptions { + pub history: bool, + pub prereleases_override: Option, + pub dry_run: bool, +} + +/// Executes the synchronization pipeline for a given repository. +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, + ); + + // Resolve Gitea credentials + let gitea_url = app_config.gitea_url.as_deref().unwrap_or_default(); + let gitea_token = app_config.gitea_token.as_deref().unwrap_or_default(); + let registry_owner = app_config.registry_owner.as_deref().unwrap_or_default(); + + if !options.dry_run && (gitea_url.is_empty() || gitea_token.is_empty() || registry_owner.is_empty()) { + bail!( + "Missing Gitea configuration. Ensure --gitea-url, --gitea-token, and --registry-owner are provided or configured." + ); + } + + let github_client = GitHubClient::new(app_config.github_token.clone()) + .context("Failed to initialize GitHub client")?; + + let gitea_client = GiteaClient::new(GiteaConfig::new( + gitea_url, + gitea_token, + registry_owner, + )) + .context("Failed to initialize Gitea client")?; + + let releases = github_client + .fetch_releases(repo_name, options.history, include_prereleases) + .await + .with_context(|| format!("Failed to fetch releases for repository '{}'", repo_name))?; + + if releases.is_empty() { + log( + "sync", + &format!("No matching releases found for repository '{}'", repo_name), + LogLevel::Info, + ); + report.repositories_processed += 1; + return Ok(()); + } + + log( + "sync", + &format!( + "Found {} release(s) to process for '{}'", + releases.len(), + repo_name + ), + LogLevel::Info, + ); + + let mut latest_synced_tag: Option = None; + + for release in &releases { + report.releases_processed += 1; + let release_type_str = if release.prerelease { "pre-release" } else { "stable release" }; + + log( + "sync", + &format!( + "Processing {} '{}' (Tag: {}) with {} Linux package asset(s)", + release_type_str, + release.name.as_deref().unwrap_or(&release.tag_name), + release.tag_name, + release.assets.len() + ), + LogLevel::Info, + ); + + if release.assets.is_empty() { + log( + "sync", + &format!( + "No Linux packages (.deb, .rpm, .pkg.tar.zst) found in release {}", + release.tag_name + ), + LogLevel::Debug, + ); + continue; + } + + for asset in &release.assets { + let target_urls = get_target_upload_urls( + gitea_url, + registry_owner, + asset.package_type, + release.prerelease, + ); + + log( + "sync", + &format!( + "Mirroring asset '{}' ({}, size: {} bytes) -> {} endpoint(s)", + asset.name, + asset.package_type.name(), + asset.size, + target_urls.len() + ), + LogLevel::Info, + ); + + if options.dry_run { + for url in &target_urls { + log( + "dry-run", + &format!("[DRY-RUN] Would upload '{}' to '{}'", asset.name, url), + LogLevel::Info, + ); + } + report.packages_simulated += target_urls.len(); + continue; + } + + // Download asset to temporary file + 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))?; + + // Upload asset to all target distribution URLs + 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, + ); + // Cleanup temp file before returning error + let _ = remove_file(&temp_file_path).await; + return Err(e); + } + } + } + + // Cleanup temp file + 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()); + } + } + + // Persist last synced tag if not 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(()) +} + +/// Downloads a GitHub release asset into a unique temporary file. +async fn download_to_temp_file( + github_client: &GitHubClient, + download_url: &str, + filename: &str, +) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + + let temp_dir = std::env::temp_dir().join("mirror-package"); + tokio::fs::create_dir_all(&temp_dir) + .await + .with_context(|| format!("Failed to create temp directory {:?}", temp_dir))?; + + let safe_filename = format!("{}-{}", now, filename.replace(['/', '\\'], "_")); + let temp_path = temp_dir.join(safe_filename); + + log( + "download", + &format!("Downloading '{}' to temporary path {:?}...", filename, temp_path), + LogLevel::Debug, + ); + + let mut response = github_client.download_asset_stream(download_url).await?; + let mut file = File::create(&temp_path) + .await + .with_context(|| format!("Failed to create temp file {:?}", temp_path))?; + + while let Some(chunk) = response.chunk().await.context("Error reading download stream")? { + file.write_all(&chunk) + .await + .context("Error writing chunk to temp file")?; + } + + file.flush() + .await + .context("Error flushing temp file to disk")?; + + Ok(temp_path) +} diff --git a/tests/config_tests.rs b/tests/config_tests.rs new file mode 100644 index 0000000..c844d4b --- /dev/null +++ b/tests/config_tests.rs @@ -0,0 +1,53 @@ +use mirror_package::config::{AppConfig, RepoConfig}; + +#[test] +fn test_normalize_repo_name() { + assert_eq!( + AppConfig::normalize_repo_name("raspberrypi/rpi-imager"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager.git"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + AppConfig::normalize_repo_name("Heroic-Games-Launcher/HeroicGamesLauncher/"), + "Heroic-Games-Launcher/HeroicGamesLauncher" + ); +} + +#[test] +fn test_repo_management() { + let mut config = AppConfig::default(); + config.add_or_update_repo(RepoConfig::new("owner/repo1", true)); + config.add_or_update_repo(RepoConfig::new("owner/repo2", false)); + + assert_eq!(config.repositories.len(), 2); + assert!(config.find_repo("owner/repo1").is_some()); + assert!(config.find_repo("https://github.com/owner/repo1").is_some()); + + assert!(config.remove_repo("owner/repo1")); + assert_eq!(config.repositories.len(), 1); + assert!(config.find_repo("owner/repo1").is_none()); +} + +#[test] +fn test_serialization_roundtrip() { + let mut config = AppConfig { + gitea_url: Some("https://gitea.example.com".to_string()), + gitea_token: Some("secret_token".to_string()), + registry_owner: Some("my-org".to_string()), + github_token: Some("gh_pat".to_string()), + repositories: Vec::new(), + }; + config.add_or_update_repo(RepoConfig::new("raspberrypi/rpi-imager", true)); + + let json_str = serde_json::to_string(&config).unwrap(); + let deserialized: AppConfig = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(config, deserialized); +} diff --git a/tests/gitea_tests.rs b/tests/gitea_tests.rs new file mode 100644 index 0000000..c65f544 --- /dev/null +++ b/tests/gitea_tests.rs @@ -0,0 +1,65 @@ +use mirror_package::gitea::get_target_upload_urls; +use mirror_package::github::PackageType; + +#[test] +fn test_debian_routing() { + let base_url = "https://gitea.example.com"; + let owner = "test-owner"; + + // Stable Debian goes to both stable and testing pools + 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 goes only to 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"; + + // Stable 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"; + + // Stable Arch + let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, false); + assert_eq!( + stable_urls, + vec!["https://gitea.example.com/api/packages/test-owner/arch/stable"] + ); + + // Pre-release Arch + let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, true); + assert_eq!( + prerelease_urls, + vec!["https://gitea.example.com/api/packages/test-owner/arch/testing"] + ); +} diff --git a/tests/github_tests.rs b/tests/github_tests.rs new file mode 100644 index 0000000..21d474e --- /dev/null +++ b/tests/github_tests.rs @@ -0,0 +1,70 @@ +use mirror_package::github::{parse_repo_owner_name, PackageType}; + +#[test] +fn test_package_classification() { + // Debian + assert_eq!( + PackageType::from_filename("rpi-imager_1.8.5_amd64.deb"), + Some(PackageType::Debian) + ); + assert_eq!( + PackageType::from_filename("heroic_2.14.1_arm64.DEB"), + Some(PackageType::Debian) + ); + + // RPM + assert_eq!( + PackageType::from_filename("rpi-imager-1.8.5-1.x86_64.rpm"), + Some(PackageType::Rpm) + ); + assert_eq!( + PackageType::from_filename("heroic-2.14.1.aarch64.RPM"), + Some(PackageType::Rpm) + ); + + // Arch Linux + assert_eq!( + PackageType::from_filename("heroic-games-launcher-bin-2.14.1-1-x86_64.pkg.tar.zst"), + Some(PackageType::Arch) + ); + assert_eq!( + PackageType::from_filename("rpi-imager-1.8.5-1-x86_64.pkg.tar.xz"), + Some(PackageType::Arch) + ); + assert_eq!( + PackageType::from_filename("package-1.0.0-1-any.pkg.tar.gz"), + Some(PackageType::Arch) + ); + assert_eq!( + PackageType::from_filename("Heroic-2.14.1.pacman"), + Some(PackageType::Arch) + ); + + // Non-package formats should be ignored + 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()); +}